Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 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. ГЛАВА 6 Классы 153 Вот некоторые другие аспекты классов, во многом зависящие от языка: поведение переопределенных конструкторов и деструкторов в дереве насле# дования; поведение конструкторов и деструкторов при обработке исключений; важность конструкторов по умолчанию (конструкторов без аргументов); время вызова деструктора или метода финализации; целесообразность переопределения встроенных операторов языка, в том числе операторов присваивания и сравнения; управление памятью при создании и уничтожении объектов или при их объяв# лении и выходе из области видимости. Подробно эти вопросы мы рассматривать не будем, но в разделе «Дополнительные ресурсы» я указал несколько хороших книг, посвященных конкретным языкам. 6.6. Следующий уровень: пакеты классов В настоящее время использование классов — лучший спо# соб достижения модульности. Однако модульность — обшир# ная тема, и она никак не ограничивается классами. В по# следние десятилетия отрасль разработки ПО развивалась во многом благодаря увеличению агрегаций, с которыми нам приходится работать. Первой агрегацией были операторы, что при сравнении с машинными командами казалось в то время большим достижением. Затем появи# лись методы, а позднее придуманы классы. Ясно, что мы могли бы лучше выполнять абстракцию и инкапсуляцию, если бы имели эффективные средства агрегации групп объектов. Java поддерживает па# кеты, а язык Ada поддерживал их уже десять лет назад. Если используемый вами язык не поддерживает пакеты непосредственно, вы можете создать собственные версии пакетов, подкрепив их стандартами программирования, такими как: конвенции именования, проводящие различие между классами, которые мож# но применять вне пакета, и классами, предназначенными только для закрыто# го использования; конвенции именования, конвенции организации кода (структура проекта) или и те, и другие конвенции, определяющие принадлежность каждого класса к тому или иному пакету; правила, определяющие возможность использования конкретных пакетов дру# гими пакетами, в том числе возможность наследования, включения или того и другого. Это еще один удачный пример различия между программированием на языке и программированием с использованием языка (см. раздел 34.4). Перекрестная ссылка О разли- чии между классами и пакетами см. также подраздел «Уровни проектирования» раздела 5.2. 154 ЧАСТЬ II Высококачественный код Контрольный список: качество классов Абстрактные типы данных Обдумали ли вы классы программы как абстрактные типы данных, оценив их интерфейсы с этой точки зрения? Абстракция Имеет ли класс главную цель? Удачное ли имя присвоено классу? Описывает ли оно глав- ную цель класса? Формирует ли интерфейс класса согласованную абстрак- цию? Ясно ли интерфейс описывает использование класса? Достаточно ли абстрактен интерфейс, чтобы вы могли не думать о реали- зации класса? Можно ли рассматривать класс как «черный ящик»? Достаточно ли полон набор сервисов класса, чтобы другие классы могли не обращаться к его внутренним данным? Исключена ли из класса нерелевантная информация? Обдумали ли вы разделение класса на классы-компоненты? Разделен ли он на максимально возможное число компонентов? Сохраняется ли целостность интерфейса при изменении класса? Инкапсуляция Сделаны ли члены класса минимально доступными? Избегает ли класс предоставления доступа к своим данным-членам? Скрывает ли класс детали реализации от других классов в максимально возможной степени, допускаемой языком программирования? Избегает ли класс предположений о своих клиентах, в том числе о произ- водных классах? Независим ли класс от других классов? Слабо ли он связан? Наследование Используется ли наследование только для моделирования отношения «яв- ляется», т. е. придерживаются ли производные классы принципа подстановки Лисков? Описана ли в документации класса стратегия наследования? Избегают ли производные классы «переопределения» непереопределяемых методов? Перемещены ли общие интерфейсы, данные и формы поведения как мож- но ближе к корню дерева наследования? Не слишком ли много уровней включают иерархии наследования? Все ли данные — члены базового класса сделаны закрытыми, а не защи- щенными? Другие вопросы реализации Класс содержит около семи элементов данных-членов или меньше? Минимально ли число встречающихся в классе непосредственных и опо- средованных вызовов методов других классов? Сведено ли к минимуму сотрудничество класса с другими классами? Все ли данные-члены инициализируются в конструкторе? http://cc2e.com/0672 Перекрестная ссылка Этот кон- трольный список позволяет определить качество классов. Об этапах создания класса см. контрольный список «Процесс программирования с псевдоко- дом» в главе 9. ГЛАВА 6 Классы 155 Спроектирован ли класс для использования полного, а не ограниченного копирования, если нет убедительной причины создания ограниченных копий? Аспекты, специфические для языков Изучили ли вы особенности работы с классами, характерные для выбран- ного языка программирования? Дополнительные ресурсы Классы в общем Meyer, Bertrand. Object%Oriented Software Construction, 2d ed. — New York, NY: Prentice Hall PTR, 1997. В этой книге Мейер рассматривает абстрактные типы данных и объясняет, как они формируют основу классов. В главах 14–16 подробно обсуждается наследование. В главе 15 Мейер приводит довод в пользу множественного наследования. Riel, Arthur J. Object%Oriented Design Heuristics. — Reading, MA: Addison#Wesley, 1996. Эта книга включает множество советов по улучшению проектирования, относя# щихся большей частью к уровню классов. Я избегал ее несколько лет, потому что она казалась слишком большой — воистину сапожник без сапог! Однако основ# ная часть книги занимает только около 200 страниц. Книга написана доступным и занимательным языком, а ее содержание сжато и практично. C++ Meyers, Scott. Effective C++: 50 Specific Ways to Improve Your Programs and Designs, 2d ed. — Reading, MA: Addison#Wesley, 1998. Meyers, Scott. More Effective C++: 35 New Ways to Improve Your Programs and Designs. — Reading, MA: Addison#Wesley, 1996. Обе книги Мейерса являются канонически# ми для программистов на C++. Они очень интересны и позволяют приобрести глу# бокие знания некоторых нюансов C++. Java Bloch, Joshua. Effective Java Programming Language Guide. — Boston, MA: Addison#Wesley, 2001. В книге Блоха можно найти много полезных советов по Java, а также описания более общих объектно#ориентированных подходов. Visual Basic Ниже указаны книги, в которых хорошо рассмотрена работа с классами в контексте Visual Basic. Foxall, James. Practical Standards for Microsoft Visual Basic .NET. — Redmond, WA: Microsoft Press, 2003. Cornell, Gary, and Jonathan Morrison. Programming VB .NET: A Guide for Experienced Programmers. — Berkeley, CA: Apress, 2002. Barwell, Fred, et al. Professional VB.NET, 2d ed. — Wrox, 2002. http://cc2e.com/0679 http://cc2e.com/0686 http://cc2e.com/0693 http://cc2e.com/0600 156 ЧАСТЬ II Высококачественный код Ключевые моменты Интерфейс класса должен формировать согласованную абстракцию. Многие проблемы объясняются нарушением одного этого принципа. Интерфейс класса должен что#то скрывать — особенности взаимодействия с системой, аспекты проектирования или детали реализации. Включение обычно предпочтительнее, чем наследование, если только вы не моделируете отношение «является». Наследование — полезный инструмент, но оно повышает сложность, что про# тиворечит Главному Техническому Императиву Разработки ПО, которым явля# ется управление сложностью. Классы — главное средство управления сложностью. Уделите их проектирова# нию столько времени, сколько нужно для достижения этой цели. ГЛАВА 7 Высококачественные методы 157 Г Л А В А 7 Высококачественные методы Содержание 7.1. Разумные причины создания методов 7.2. Проектирование на уровне методов 7.3. Удачные имена методов 7.4. Насколько объемным может быть метод? 7.5. Советы по использованию параметров методов 7.6. Отдельные соображения по использованию функций 7.7. Методы#макросы и встраиваемые методы Связанные темы Этапы конструирования методов: раздел 9.3 Классы: глава 6 Общие методики проектирования: глава 5 Архитектура ПО: раздел 3.5 В главе 6 мы подробно рассмотрели создание классов. В этой главе мы обратим внимание на методы и характеристики, отличающие хорошие методы от плохих. Если вам хотелось бы сначала разобраться в вопросах, влияющих на проектиро# вание методов, прочитайте главу 5 и потом вернитесь к этой главе. Некоторые важные атрибуты высококачественных методов обсуждаются также в главе 8. Если вас больше интересуют этапы создания методов и классов, см. главу 9. Прежде чем перейти к деталям, определим два базовых термина. Что такое «метод»? Метод — это отдельная функция или процедура, выполняющая одну задачу. В раз# личных языках методы могут называться по#разному, но их суть от этого не меня# ется. Иногда макросы C и C++ также полезно рассматривать как методы. Многие советы по созданию высококачественных методов относятся и к макросам. Что такое высококачественный метод? На этот вопрос ответить сложнее. Возможно, лучше всего просто показать, что не является высококачественным методом. Вот пример низкокачественного метода: http://cc2e.com/0778 158 ЧАСТЬ II Высококачественный код Пример низкокачественного метода (C++) void HandleStuff( CORP_DATA & inputRec, int crntQtr, EMP_DATA empRec, double & estimRevenue, double ytdRevenue, int screenX, int screenY, COLOR_TYPE & newColor, COLOR_TYPE & prevColor, StatusType & status, int expenseType ) { int i; for ( i = 0; i < 100; i++ ) { inputRec.revenue[i] = 0; inputRec.expense[i] = corpExpense[ crntQtr ][ i ]; } UpdateCorpDatabase( empRec ); estimRevenue = ytdRevenue * 4.0 / (double) crntQtr; newColor = prevColor; status = SUCCESS; if ( expenseType == 1 ) { for ( i = 0; i < 12; i++ ) profit[i] = revenue[i] expense.type1[i]; } else if ( expenseType == 2 ) { profit[i] = revenue[i] expense.type2[i]; } else if ( expenseType == 3 ) profit[i] = revenue[i] expense.type3[i]; } Что тут не так? Подскажу: вы должны найти минимум 10 недостатков. Составив свой список, сравните его с моим. Неудачное имя: HandleStuff() ничего не говорит о роли метода. Метод недокументирован (вопрос документирования не ограничивается отдель# ными методами и обсуждается в главе 32). Метод плохо форматирован. Физическая организация кода почти не дает пред# ставления о его логической организации. Стратегии форматирования исполь# зуются непоследовательно: сравните стили операторов if с условиями expenseType == 2 и expenseType == 3 (о форматировании см. главу 31). Входная переменная inputRec внутри метода изменяется. Если это входная пе# ременная, изменять ее нежелательно (в случае C++ ее следовало бы объявить как const). Если изменение значения предусмотрено, переменную не стоило называть inputRec. Метод читает и изменяет глобальные переменные: читает corpExpense и изме# няет profit. Взаимодействие этого метода с другими следовало бы сделать бо# лее непосредственным, без использования глобальных переменных. Цель метода размыта. Он инициализирует ряд переменных, записывает дан# ные в БД, выполняет вычисления — все эти действия не кажутся связанными между собой. Метод должен иметь одну четко определенную цель. ГЛАВА 7 Высококачественные методы 159 Метод не защищен от получения плохих данных. Если переменная crntQtr равна 0, выражение ytdRevenue * 4.0 / (double) crntQtr вызывает ошибку деления на 0. Метод использует несколько «магических» чисел: 100, 4.0, 12, 2 и 3 (о магичес# ких числах см. раздел 12.1). Параметры screenX и screenY внутри метода не используются. Параметр prevColor передается в метод неверно: он передается по ссылке (&), но значение ему внутри метода не присваивается. Метод принимает слишком много параметров. Как правило, чтобы параметры можно было охватить умом, их должно быть не более 7 — этот метод прини# мает 11. Параметры представлены таким неудобочитаемым образом, что боль# шинство разработчиков даже не попытаются внимательно изучить их или хотя бы подсчитать. Параметры метода плохо упорядочены и не документированы (об упорядоче# нии параметров см. эту главу, о документировании — главу 32). Если не считать сами компьютеры, методы — величайшее изобретение в области компьютерных наук. Методы облег# чают чтение и понимание программ в большей степени, чем любая другая возможность любого языка программирова# ния, и оскорблять столь заслуженных в мире программиро# вания деятелей таким кодом, что был приведен выше, — настоящее преступление. Кроме того, методы — самый эффективный способ умень# шения объема и повышения быстродействия программ. Представьте, насколько объемнее были бы ваши програм# мы, если б вместо каждого вызова метода нужно было вставить соответствующий код. Представьте, насколько сложнее было бы оптимизировать код, если бы он был распространен по всей программе, а не локализован в одном методе. Програм# мирование, каким мы его знаем сегодня, оказалось бы без методов невозможным. «Хорошо, — скажете вы. — Я уже знаю, что методы очень полезны и постоянно их использую. Чего ж вы от меня хотите?» Я хочу, чтобы вы поняли, что есть много веских причин, а также правильных и неправильных способов создания методов. Будучи студентом факультета инфор# матики, я думал, что главная причина создания методов — предотвращение дуб# лирования кода. Во вводном учебнике, по которому я учился, полезность методов обосновывалась тем, что предотвращение дублирования кода делает программу более простой в разработке, отладке, документировании и сопровождении. Точ# ка. Если не считать синтаксические детали использования параметров и локаль# ных переменных, на этом обсуждение методов в той книге заканчивалось. Такое объяснение теории и практики использования методов нельзя считать ни удач# ным, ни полным. В следующих разделах я постараюсь это исправить. http://cc2e.com/0799 Перекрестная ссылка Классы также претендуют на роль ве- личайшего изобретения в обла- сти информатики. Об эффек- тивном использовании классов см. главу 6. 160 ЧАСТЬ II Высококачественный код 7.1. Разумные причины создания методов Ниже я привел список причин создания метода. Они несколько перекрываются и не исключают одна другую. Снижение сложности Самая важная причина создания метода — снижение сложности программы. Создайте метод для сокрытия инфор# мации, чтобы о ней можно было не думать. Конечно, при написании метода думать о ней придется, но после этого вы сможете забыть о деталях и ис# пользовать метод, не зная о его внутренней работе. Другие причины создания методов — минимизация объема кода, облегчение сопровождения программы и снижение числа ошибок — также хороши, но без абстрагирующей силы методов сложные программы было бы невозможно охватить умом. Одним из признаков того, что метод следует разделить, является глубокая вложен# ность внутренних циклов или условных операторов. Упростите такой метод, вы# делив вложенную часть в отдельный метод. Формирование понятной промежуточной абстракции Выделение фраг# мента кода в удачно названный метод — один из лучших способов документиро# вания его цели. Вместо того, чтобы работать с фрагментами вида: if ( node <> NULL ) then while ( node.next <> NULL ) do node = node.next leafName = node.name end while else leafName = ”” end if вы можете иметь дело с чем#нибудь вроде: leafName = GetLeafName( node ) Новый метод так прост, что для документирования достаточно присвоить ему удачное имя. В сравнении с первоначальными восемью строками кода имя мето# да формирует абстракцию более высокого уровня, что облегчает чтение и пони# мание кода, а также снижает его сложность. Предотвращение дублирования кода Несомненно, самая популярная причина создания метода — желание избежать дублирования кода. Действительно, вклю# чение похожего кода в два метода указывает на ошибку декомпозиции. Уберите повторяющийся фрагмент из обоих методов, поместите его общую версию в ба# зовый класс и создайте два специализированных метода в подклассах. Вы также можете выделить общий код в отдельный метод и вызвать его из двух первона# чальных методов. В результате программа станет компактнее. Изменять ее станет проще, так как в случае чего вам нужно будет изменить только один метод. Код станет надежнее, потому что для его проверки нужно будет проанализировать только один фрагмент. Изменения будут реже приводить к ошибкам, поскольку вы не сможете по невнимательности внести в идентичные фрагменты програм# мы чуть различающиеся изменения. |