Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 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. |