Главная - Литература

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 [46] 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294

Базовый класс формулирует ожидания и ограничения, которым должен будет соответствовать производный класс (Meyers, 1998).

Если производный класс не собирается полностью придерживаться контракта, определенного интерфейсом базового класса, наследование выполнять не стоит. Попробуйте вместо этого применить включение или внести изменение на более высоком уровне иерархии наследования.

Проектируйте и документируйте классы с учетом возможности наследования или запретите его Наследование повышает сложность программы, и в этом смысле оно может быть опасным. Поэтому гуру программирования на Java Джошуа Блох и сказал: «Проектируйте и документируйте классы с учетом возможности наследования или запретите его». Если при проектировании класса вы решили, что он не должен поддерживать наследование, не объявляйте его члены как virtual в случае С++ или overridable в случае Microsoft Visual Basic; если вы программируете на Java, объявите члены такого класса как final.

Соблюдайте принцип подстановки Дисков (Liskov Substitution Principle, ISP)

Барбара Лисков как-то заявила, что наследование стоит использовать, только если производный класс действительно «является» более специализированной версией базового класса (Liskov, 1988). Энди Хант и Дэйв Томас сформулировали LSP так: «Клиенты должны иметь возможность использования подклассов через интерфейс базового класса, не замечая никаких различий» (Hunt and Thomas, 2000).

Иначе говоря, все методы базового класса должны иметь в каждом производном классе то же значение.

Если у вас есть базовый KR2iCC Account (счет) и производные классы CbeckingAccount (счет до востребования), SavingsAccount (депозитный счет) и AutoLoanAccount (счет ссуд), то при вызове каких бы то ни было методов кягсс2 Account в любом из его подтипов программист не должен заботиться о подтипе конкретного объекта «счет».

При соблюдении принципа подстановки Лисков наследование - мощное средство снижения сложности, позволяющее программисту сосредоточиться на общих атрибутах объекта, не волнуясь об его деталях. Если же программист должен постоянно помнить о семантических различиях реализаций подклассов, наследование только повьшгает сложность. Так, в нашем примере программисту пришлось бы думать: «Если я вызываю метод InterestRateQ (процентная ставка) класса CbeckingAccount или SavingsAccount, он возвращает процент, который банк выплачивает клиенту, однако метод InterestRateQ класса AutoLoanAccount возвращает процент, выплачиваемый клиентом банку, поэтому я должен изменить знак результата». В соответствии с LSP, в данном случае класс AutoLoanAccount не должен быть производным от класса Account, потому что методы InterestRateQ в этих классах имеют разные семантические значения.

Убедитесь, что вы наследуете только то, что хотите наследовать

Производный класс может наследовать интерфейсы методов-членов, их реализации или и то, и другое (табл. 6-1).



Табл. 6-1. Разновидности наследуемых методов

Переопределение Переопределение

метода возможно метода невозможно

Реализация по умолчанию Переопределяемый метод Непереопределяемый метод, имеется

Реализация по умолчанию Абстрактный Этот вариант не использует-отсутствует переопределяемый метод ся (нет смысла в том, чтобы

оставить метод без определения, не позволив его переопределить).

Как следует из таблицы, наследуемые методы могут относиться к одной из трех категорий:

абстрактный переопределяемый метод: производный класс наследует интерфейс метода, но не его реализацию;

переопределяемый метод: производный класс наследует интерфейс метода и его реализацию по умолчанию, а также может переопределить эту реализацию;

непереопределяемый метод: производный класс наследует интерфейс метода и его реализацию по умолчанию, переопределить которую не может.

Создавая новый класс при помощи наследования, обдумайте тип наследования каждого метода-члена. Не наследуйте реализацию только потому, что вы наследуете интерфейс, и не наследуйте интерфейс только для того, чтобы унаследовать реализацию. Если вам нужна реализация класса, но не его интерфейс, используйте включение, а не наследование.

Не «переопределяйте» иепереопределяемые методы-члены И С++, и Java позволяют программисту переопределить непереопределяемый метод-член - ну, или что-то вроде того. Если функция объявлена в базовом классе как private, в производном классе можно создать функцию с тем же именем. Программист, изучающий код производного класса, может прийти к ложному выводу, что эта функция является полиморфной, хотя на самом деле это не так - просто у нее то же имя. Иначе сформулировать это правило можно так: «Не используйте имена непереопределяемых методов базового класса в производных классах».

Перемещайте общие интерфейсы, данные и формы поведения на как можно более высокий уровень иерархии наследования Чем ближе интерфейсы, данные и формы поведения к корню дерева наследования, тем легче производным классам их использовать. Какой уровень считать слишком высоким? Руководствуйтесь соображениями абстракции. Если вам кажется, что перемещение метода на более высокий уровень нарушит абстракцию соответствующего класса, не делайте этого.

С подозрением относитесь к классам, объекты которых создаются в единственном экземпляре Использование единственного экземпляра класса может указывать на то, что вы спутали объекты с классами. Подумайте, можно ли просто создать объект вместо нового класса. Можно ли конкретный производный класс представить только данными, а не отдельным классом? Шаблон Одиночка (Singleton) - примечательное исключение из этого правила.



С подозрением относитесь к базовым классам, имеющим только один производный класс Когда я вижу базовый класс, имеющий только один производный класс, то начинаю подозревать, что какой-то программист «проектировал наперед» - пытался предвосхитить будущие потребности, скорее всего не понимая их в полной мере. Лучший способ подготовки к будущей работе - не проектировать дополнительные уровни базовых классов, которые «когда-нибудь могут понадобиться», а написать максимально ясный, понятный и простой код. Это означает, что иерархию наследования не надо усложнять без крайней нужды.

С подозрением относитесь к классам, которые переопределяют метод, оставляя его пустым Как правило, это говорит о неудачном проектировании базового класса. Допустим, вы создали класс Cat, включающий метод ScratcbQ (царапать), но после обнаружили, что некоторые коты лишены когтей и не могут царапаться. Вы могли бы унаследовать от класса Cat класс ScratchlessCat, переопределив в нем метод ScratcbQ так, чтобы он ничего не делал. Однако этот подход связан с рядом проблем.

Он нарушает абстракцию (контракт интерфейса) класса Cat, изменяя семантику его интерфейса.

При расширении на другие производные классы этот подход быстро становится неуправляемым. 4то будет, когда вы найдете кота без хвоста? Или кота, который не ловит мышей? Или кота, который не пьет молоко? В итоге у вас могут появиться производные классы вроде ScratcblessTaillessMicelessMilklessCat.

Код, написанный по этой методике, трудно сопровождать, потому что со временем поведение производных классов начинает сильно отличаться от интерфейсов и форм поведения базовых классов.

Исправлять эту проблему следует не в базовом классе, а в первоначальном классе Cat. Создайте класс Claws (когти) и включите его в класс Cats. Корень наших бед - предположение, что все коты царапаются; предложенный способ позволит устранить причину проблемы, а не бороться с ее следствиями.

Избегайте многоуровневых иерархий наследования Объектно-ориентированное программирование поддерживает массу способов управления сложностью. Но использование любого мощного средства сопряжено с риском, и некоторые объектно-ориентированные подходы часто повышают сложность вместо того, чтобы снижать ее.

Артур Риэль в прекрасной книге «Object-Oriented Design Heuristics» (Kiel, 1996) предлагает ограничивать иерархии наследования максимум шестью уровнями. Он основывает свой совет на «магическом числе 7+2», но мне кажется, что это слишком оптимистично. Опыт подсказывает мне, что большинству людей трудно удержать в уме более двух или трех уровней наследования сразу. «Магическое число 7+2» скорее характеризует максимально допустимое общее количество подклассов базового класса, а не уровней иерархии наследования.

Создание многоуровневых иерархий наследования значительно повышает число ошибок (Basili, Briand, and Melo, 1996). Тот, кто занимался отладкой сложной иерархии наследования, знает причину этого. Многоуровневые иерархии повышают сложность, что диаметрально противоположно цели наследования. Помните про



0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 [46] 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294



0.009