Главная страница

Руководство по стилю программирования и конструированию по


Скачать 7.6 Mb.
НазваниеРуководство по стилю программирования и конструированию по
Дата18.05.2023
Размер7.6 Mb.
Формат файлаpdf
Имя файлаCode_Complete.pdf
ТипРуководство
#1139697
страница20 из 104
1   ...   16   17   18   19   20   21   22   23   ...   104
ГЛАВА 6 Классы
145
ции это неуместно: имя метода
DoCommand() было бы настолько туманным, что почти утратило бы всякий смысл, тогда как блоки
case довольно информативны.
Делайте все данные закрытыми, а не защищенными Как говорит Джошуа
Блох, «наследование нарушает инкапсуляцию» (Bloch, 2001). Выполняя наследо- вание от класса, вы получаете привилегированный доступ к его защищенным методам и данным. Если производному классу на самом деле нужен доступ к ат- рибутам базового класса, включите в базовый класс защищенные методы доступа.
Множественное наследование
Наследование — мощный и... довольно опасный инструмент.
В некотором смысле наследование похоже на цепную пилу:
при соблюдении мер предосторожности оно может быть невероятно полезным, но при неумелом обращении послед- ствия могут оказаться очень и очень серьезными.
Если наследование — цепная пила, то множественное на- следование — это старинная цепная пила с барахлящим мотором, не имеющая предохранителей и не поддержива- ющая автоматического отключения. Иногда такой инструмент может пригодить- ся, но большую часть времени его лучше хранить в гараже под замком.
Некоторые эксперты рекомендуют широкое применение множественного насле- дования (Meyer, 1997), но по опыту могу сказать, что оно полезно главным обра- зом только при создании «миксинов» — простых классов, позволяющих добавить ряд свойств в другой класс. Миксины называются так потому, что они позволяют
«подмешать (mix in)» свойства в производные классы. Миксинами могут быть классы вроде
Displayable, Persistent, Serializable или Sortable. Миксины почти всегда явля- ются абстрактными и не поддерживают создания экземпляров независимо от других объектов.
Миксины требуют множественного наследования, но пока все миксины по-насто- ящему независимы друг от друга, вы можете не бояться классической проблемы,
связанной с ромбовидной схемой наследования. Кроме того, «объединяя» атри- буты, они делают проект системы понятнее. Программисту легче разобраться с объектом, использующим миксины
Displayable и Persistent, а не 11 более конкрет- ных методов, которые понадобились бы для реализации этих двух свойств в про- тивном случае.
Похоже, разработчики Java и Visual Basic понимали ценность миксинов, разрешив множественное наследование интерфейсов, но только единичное наследование классов. C++ поддерживает множественное наследование и интерфейсов, и реа- лизации. Используйте множественное наследование, только тщательно рассмот- рев все альтернативные варианты и проанализировав влияние выбранного под- хода на сложность и понятность системы.
Почему правил наследования так много?
В этом разделе были описаны многие правила избавления от проблем,
связанных с наследованием. Все эти правила подразумевают, что
насле-
дование часто противоречит главному техническому императиву програм-
С множественным наследовани- ем в C++ связан один неоспори- мый факт: оно открывает ящик
Пандоры, полный проблем, ко- торые просто невозможны при единичном наследовании.
Скотт Мейерс
(Scott Meyers)

146
ЧАСТЬ II Высококачественный код
мирования — управлению сложностью. Ради управления сложностью относитесь к наследованию с подозрением. Вот как использовать наследование и включение:

если несколько классов имеют общие данные, но не фор- мы поведения, создайте общий объект, который можно было бы включить во все эти классы;

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

если несколько классов имеют общие данные и формы поведения, сделайте эти классы производными от общего базового класса, определяющего общие дан- ные и методы;

используйте наследование, если хотите, чтобы интерфейс определялся базо- вым классом, и включение, если хотите сами контролировать интерфейс.
Методы-члены и данные-члены
Ниже я даю несколько советов по эффективной реализации методов-членов и данных-членов.
Включайте в класс как можно меньше методов
В одном исследовании программ на C++ было обнаружено, что большему числу методов в расчете на один класс соответствует большее число изъянов (Basili, Briand,
and Melo, 1996). Однако важнее оказались другие конкурирующие факторы, в том числе многоуровневые иерархии наследования, большое число методов, вызыва- емых из класса, и сильное сопряжение между классами. Разрабатывая класс, стре- митесь к оптимальному соответствию между этими факторами и минимальным числом методов.
Блокируйте неявно сгенерированные методы и операторы, которые вам
не нужны Иногда некоторые возможности, такие как создание объекта или при- сваивание, целесообразно блокировать. Вам может показаться, что сделать это невозможно, потому что компилятор генерирует эти операции автоматически.
Однако вы можете запретить их использование в клиентском коде, объявив кон- структор, оператор присваивания или другой метод или оператор как
private.
(Создание закрытого конструктора — стандартный способ определения класса- одиночки, о чем см. ниже.)
Минимизируйте число разных методов, вызываемых классом Одно иссле- дование показало, что число дефектов в коде класса статистически коррелирует с общим числом методов, вызываемых классом (Basili, Briand, and Melo, 1996). То же исследование показало, что число дефектов в коде класса повышается и при увеличении числа используемых в нем классов. Эти концепции иногда называют
«коэффициентом разветвления по выходу (fan out)».
Избегайте опосредованных вызовов методов других классов Непосред- ственные связи довольно опасны. Опосредованные связи, такие как
account.Con-
tactPerson().DaytimeContactInfo().PhoneNumber(), опасны еще больше. В связи с этим ученые сформулировали «Правило Деметры (Law of Demeter)» (Lieberherr and
Holland, 1989), которое гласит, что Объект A может вызывать любые из собствен- ных методов. Если он создает Объект B, он может вызывать любые методы Объекта
Перекрестная ссылка О слож- ности см. подраздел «Главный
Технический Императив Разра- ботки ПО: управление сложно- стью» раздела 5.2.
Перекрестная ссылка О методах в общем см. главу 7.

ГЛАВА 6 Классы
147
B, но ему не следует вызывать методы объектов, возвраща- емых Объектом B. В нашем случае это означает, что вызов
account . ContactPerson() приемлем, однако вызова account.-
ContactPerson() .DaytimeContactInfo() следовало бы избежать.
Это упрощенное объяснение — подробнее см. в книгах, ука- занных в конце главы.
Вообще минимизируйте сотрудничество класса с дру-
гими классами Старайтесь свести к минимуму все следу- ющие показатели:

число видов создаваемых объектов;

число непосредственно вызываемых методов созданных объектов;

число вызовов методов, принадлежащих объектам, возвращенным другими созданными объектами.
Конструкторы
Советы по использованию конструкторов почти не зависят от языка (по крайней мере это касается C++, Java и Visual Basic). С деструкторами связано больше раз- личий — см. материалы, указанные в разделе «Дополнительные ресурсы».
Инициализируйте по мере возможности все данные-члены во всех кон-
структорах Инициализация всех данных-членов во всех конструкторах — про- стой прием защитного программирования.
Создавайте классы-одиночки с помощью закрытого
конструктора Если вы хотите определить класс, позво- ляющий создать только один объект, скройте все конструк- торы класса и создайте статический метод
GetInstance(), пре- доставляющий доступ к единственному экземпляру класса:
Пример создания класса-одиночки с помощью
закрытого конструктора (Java)
public class MaxId {
// конструкторы и деструкторы
Закрытый конструктор.
private MaxId() {
}
// открытые методы
Открытый метод, предоставляющий доступ к единственному экземпляру класса.
public static MaxId GetInstance() {
return m_instance;
}
// закрытые члены
Дополнительные сведения Хо- рошее обсуждение «Правила
Деметры» см. в книгах «Pragma- tic Programmer» (Hunt and Tho- mas, 2000), «Applying UML and
Patterns» (Larman, 2001) и «Fun- damentals of Object-Oriented De- sign in UML» (Page-Jones, 2000).
Дополнительные сведения Ана- логичный код, написанный на
C++, был бы очень похож. Под- робнее см. раздел 26 книги «More
Effective C++» (Meyers, 1998).
>
>

148
ЧАСТЬ II Высококачественный код
Единственный экземпляр класса.
private static final MaxId m_instance = new MaxId();
}
Закрытый конструктор вызывается только при инициализации статического объек- та
m_instance. Для обращения к классу-одиночке MaxId нужно просто вызвать метод
MaxId. GetInstance().
Если сомневаетесь, выполняйте полное копирование, а не ограниченное
Одним из главных аспектов работы со сложными объектами является выбор типа их копирования: полного или ограниченного. Полная копия (deep copy) — это почленная копия данных-членов объекта; ограниченная копия (shallow copy)
обычно просто указывает или ссылается на исходный объект, хотя конкретные значения «полного» и «ограниченного» копирования могут различаться.
Мотивом создания ограниченных копий обычно бывает повышение быстродей- ствия программы. Однако создание нескольких копий крупных объектов редко приводит к заметному снижению быстродействия, хотя и выглядит эстетически непривлекательно. Полное копирование некоторых объектов действительно мо- жет снижать быстродействие, но программисты обычно очень плохо определя- ют, какой код вызывает проблемы (см. главу 25). Повышение сложности едва ли можно оправдать сомнительным улучшением быстродействия кода, поэтому, если не доказано обратное, лучше выполнять полное копирование.
Полные копии легче в реализации и сопровождении, чем ограниченные. При ограниченном копировании нужно написать не только специфический для объекта код, но и код подсчета ссылок, безопасного сравнения объектов, их безопасного уничтожения и т. д. Такой код может быть источником ошибок, поэтому без вес- кой причины создавать его не следует.
Если вы находите, что вам все-таки нужно ограниченное копирование, прекрас- ное обсуждение этого подхода в контексте C++ см. в разделе 29 книги Скотта
Мейерса «More Effective C++» (Meyers, 1996). В книге Мартина Фаулера «Refactoring»
(Fowler, 1999) описываются специфические действия, нужные для преобразова- ния ограниченных копий в полные и наоборот [Фаулер называет их объектами- ссылками (reference objects) и объектами-значениями (value objects)].
6.4. Разумные причины создания классов
Если вы верите всему, что читаете, у вас могло сложиться впечатление, что единственная причина создания класса —
моделирование объектов реального мира. На самом деле это весьма далеко от истины. Список разумных причин созда- ния класса приведен ниже.
Моделирование объектов реального мира Пусть моде- лирование объектов реального мира — не единственная причина создания класса, но от этого она не становится менее хорошей! Создайте класс для каждого объекта реаль-
Перекрестная ссылка Причины создания классов и методов во многом перекрываются (см.
раздел 7.1).
Перекрестная ссылка Об иден- тификации объектов реального мира см. подраздел «Определи- те объекты реального мира»
раздела 5.3.
>

ГЛАВА 6 Классы
149
ного мира, моделируемого вашей программой. Поместите нужные объекту дан- ные в класс и создайте сервисные методы, моделирующие поведение объекта.
Примеры подобного моделирования см. в разделе 6.1.
Моделирование абстрактных объектов Другой разумной причиной созда- ния класса является моделирование
абстрактного объекта — объекта, который не существует в реальном мире, но является абстракцией других конкретных объек- тов. Прекрасный пример — классический объект
Shape (фигура). Объекты Circle
(окружность) и
Square (прямоугольник) существуют на самом деле, тогда как класс
Shape — это абстракция конкретных фигур.
В мире программирования редко встречаются готовые абстракции вроде
Shape, из- за чего поиск ясных абстракций усложняется. Процесс извлечения абстракций из разнообразия сущностей реального мира недетерминирован, и формирование абстракций может быть основано на разных принципах. Если бы нам не были из- вестны окружности, прямоугольники, треугольники и другие геометрические фи- гуры, мы могли бы прийти в итоге к более необычным фигурам, таким как «каба- чок», «брюква» и «Понтиак Ацтек». Нахождение адекватных абстрактных объектов
— одна из главных проблем объектно-ориентированного проектирования.
Снижение сложности Снижение сложности — самая важная причи- на создания класса. Создайте класс для сокрытия информации, чтобы о ней можно было не думать. Конечно, вам придется думать о ней при написании класса, но после этого вы сможете забыть о деталях и использовать класс, не зная о его внутренней работе. Другие причины создания классов —
минимизация объема кода, облегчение сопровождения программы и снижение числа ошибок — тоже хороши, но без абстрагирующей силы классов сложные программы было бы невозможно охватить умом.
Изоляция сложности Как бы ни проявлялась сложность — в форме запутан- ных алгоритмов, крупных наборов данных, замысловатых протоколов коммуни- кации, — она является источником ошибок. При возникновении ошибки ее будет проще найти, если она будет локализована в классе, а не распределена по всему коду. Изменения, обусловленные исправлением ошибки, не повлияют на осталь- ной код, потому что вам придется исправить только один класс. Если вы найдете более эффективный, простой или надежный алгоритм, им будет легче заменить старый алгоритм, изолированный в классе. Во время разработки вам будет про- ще попробовать несколько вариантов проектирования и выбрать наилучший.
Сокрытие деталей реализации Еще одна прекрасная причина создания класса
— сокрытие деталей реализации, и таких сложных, как мудреный способ доступа к БД, и столь банальных, как отдельный элемент данных, хранимый в форме чис- ла или строки.
Ограничение влияния изменений Изолируйте области вероятных изменений,
чтобы влияние изменений ограничивалось пределами одного или нескольких классов. Проектируйте приложение так, чтобы области вероятных изменений можно было изменить с максимальной легкостью. В число областей вероятных изменений входят зависимости от оборудования, подсистема ввода/вывода, слож- ные типы данных и бизнес-правила. Некоторые частые источники изменений

150
ЧАСТЬ II Высококачественный код описаны в подразделе «Скрывайте секреты (к вопросу о сокрытии информации)»
раздела 5.3.
Сокрытие глобальных данных Используя глобальные данные, вы можете скрыть детали их реализации за интер- фейсом класса. Обращение к глобальным данным через ме- тоды доступа имеет ряд преимуществ в сравнении с их не- посредственным использованием. Вы можете менять структуру данных, не изме- няя программу. Вы можете следить за доступом к данным. Кроме того, использо- вание методов доступа подталкивает к размышлениям о том, на самом ли деле данные глобальны; часто оказывается, что «глобальные данные» на самом деле являются просто данными какого-то объекта.
Упрощение передачи параметров в методы Если вы передаете один пара- метр в несколько методов, это может указывать на необходимость объединения этих методов в класс, чтобы они могли использовать параметр как данные объекта.
Упрощение передачи параметров в методы само по себе не цель, но передача крупных объемов данных наводит на мысль, что другая организация классов могла бы быть более эффективной.
Создание центральных точек управления Управлять каждой задачей в одном месте — разумная идея. Управле- ние может принимать разные формы. Знание числа элемен- тов таблицы — одна форма. Управление файлами, соедине- ниями с БД, принтерами и другими устройствами — другая.
Использование одного класса для чтения и записи БД явля- ется формой централизованного управления. Если БД нужно будет преобразовать в однородный файл или данные «в памяти», изменения придется внести только в один класс.
Идея централизованного управления похожа на сокрытие информации, но она имеет уникальную эвристическую силу, так что не забудьте добавить ее в свой инструментарий.
Облегчение повторного использования кода Код, разбитый на грамотно орга- низованные классы, легче повторно использовать в других программах, чем тот же код, помещенный в один более крупный класс. Даже если фрагмент вызывает- ся только из одного места программы и вполне понятен в составе более крупно- го класса, подумайте, может ли он понадобиться в другой программе. Если да, стоит поместить его в отдельный класс.
В Лаборатории проектирования ПО NASA были изучены десять проек- тов, в которых энергично преследовалось повторное использование кода
(McGarry, Waligora, and McDermott, 1989). И при объектно-, и при функ- ционально-ориентированном подходах разработчикам первоначально не удалось достичь этой цели, потому что в предыдущих проектах не была создана достаточная база кода. Впоследствии при работе над функциональными проектами около 35%
кода удалось взять из предыдущих проектов. В проектах, основанных на объект- но-ориентированном подходе, этот показатель составил 70%. Если благодаря заб- лаговременному планированию можно избежать написания 70% кода, грех этим не воспользоваться!
Перекрестная ссылка О сокры- тии информации см. подраздел
«Скрывайте секреты (к вопро- су о сокрытии информации)»
раздела 5.3.
Перекрестная ссылка О пробле- мах, связанных с глобальными данными, см. раздел 13.3.

ГЛАВА 6 Классы
151
Заметьте, что ядро подхода NASA к созданию повторно ис- пользуемых классов не включает «проектирование для по- вторного использования». Классы, претендующие на повтор- ное использование, определяют в NASA в конце проектов.
Все действия по упрощению повторного использования классов выполняются как специальный проект в конце ос- новного проекта или как первый этап нового проекта. Этот подход помогает предотвращать «позолоту» — создание не- нужной функциональности, только повышающей сложность.
Планирование создания семейства программ Если вы ожидаете, что програм- му придется изменять, разумно изолировать области предполагаемых изменений в отдельных классах. После этого вы можете изменять классы, не влияя на остальную часть программы, или вообще заменить их на абсолютно новые классы. Размыш- ление о том, как может выглядеть целое семейство программ, а не просто одна программа, — эффективный эвристический принцип предвосхищения целых категорий изменений (Parnas, 1976).
Как-то я руководил группой, работавшей над рядом программ, упрощавших заклю- чение договоров страхования. Мы должны были адаптировать каждую программу к отдельным тарифам, формату отчетов конкретного клиента и т. д. Однако мно- гие части программ были похожи: например, классы ввода данных о потенциаль- ных заказчиках, классы, сохранявшие информацию в БД, классы просмотра тари- фов и т. д. Мы организовали программу так, чтобы каждая «изменчивая» часть на- ходилась в отдельном классе. Создание первоначальной программы заняло три месяца или около того, но зато когда к нам обращался новый клиент, мы просто переписывали несколько классов и включали их в остальной код. Несколько дней работы и — вуаля! — специализированное приложение!
Упаковка родственных операций Если создание класса не удается обосновать сокрытием информации, совместным доступом к данным или обеспечением гиб- кости программы, вы все же можете упаковать наборы операций в более осмыс- ленные группы, такие как группы тригонометрических функций, статистических функций, методов работы со строками, методов манипулирования битами, гра- фических методов и т. д. Класс — не единственное средство объединения родствен- ных операций. В зависимости от конкретного языка для этого также можно ис- пользовать пакеты, пространства имен или заголовочные файлы.
Выполнение специфического вида рефакторинга Создание новых классов предусматривают многие специфические виды рефакторинга (см. главу 24), та- кие как разделение одного класса на два, сокрытие делегата, удаление класса-по- средника и формирование класса-расширения. Создание этих новых классов может быть мотивировано стремлением к лучшему выполнению какой-либо задачи из описанных в данном разделе.
Классы, которых следует избегать
Хотя в целом классы очень полезны, работая с ними, вы можете столкнуться с проблемами. Ниже описаны классы, создавать которые не следует.
Перекрестная ссылка О реали- зации минимального объема необходимой функциональности см. подраздел «Программа со- держит код, который может когда-нибудь понадобиться»
раздела 24.2.

152
ЧАСТЬ II Высококачественный код
Избегайте создания «божественных» классов Избегайте создания классов,
которые все знают и все могут. Если класс извлекает и задает данные других классов с использованием методов
Get() и Set() (т. е. вмешивается в их дела и указывает им, что делать), спросите себя, не следует ли его функциональность реализовать в тех классах, а не выделять в божественный класс (Riel, 1996).
Устраняйте нерелевантные классы Если класс имеет только данные, но не формы поведения, спросите себя, дей- ствительно ли это класс. Возможно, этот класс следует раз- жаловать, сделав его данные-члены атрибутами одного или нескольких других классов.
Избегайте классов, имена которых напоминают глаголы Как правило,
класс, имеющий только формы поведения, но не данные, на самом деле классом не является. Подумайте о превращении класса вроде
DatabaseInitialization() или
StringBuilder() в метод какого-нибудь другого класса.
Резюме причин создания класса
Вот список разумных причин создания класса:

моделирование объектов реального мира;

моделирование абстрактных объектов;

снижение сложности;

изоляция сложности;

сокрытие деталей реализации;

ограничение влияния изменений;

сокрытие глобальных данных;

упрощение передачи параметров в методы;

создание центральных точек управления;

облегчение повторного использования кода;

планирование создания семейства программ;

упаковка родственных операцией;

выполнение специфического вида рефакторинга.
6.5. Аспекты, специфические для языков
Использование классов в разных языках программирования имеет интересные различия. Рассмотрим, например, переопределение метода-члена в производном классе при реализации полиморфизма. В Java все методы переопределяемы по умолчанию, а чтобы в производном классе метод нельзя было переопределить, его нужно объявить как
final. В C++ методы по умолчанию непереопределяемы. Чтобы сделать метод переопределяемым, его нужно объявить в базовом классе как
virtual.
В Visual Basic переопределяемый метод должен быть объявлен в базовом классе как
overridable, а в производном классе нужно использовать ключевое слово overrides.
Перекрестная ссылка Такой вид класса обычно называют структу- рой. О структурах см. раздел 13.1.

1   ...   16   17   18   19   20   21   22   23   ...   104


написать администратору сайта