Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 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 ) Новый метод так прост, что для документирования достаточно присвоить ему удачное имя. В сравнении с первоначальными восемью строками кода имя мето- да формирует абстракцию более высокого уровня, что облегчает чтение и пони- мание кода, а также снижает его сложность. Предотвращение дублирования кода Несомненно, самая популярная причина создания метода — желание избежать дублирования кода. Действительно, вклю- чение похожего кода в два метода указывает на ошибку декомпозиции. Уберите повторяющийся фрагмент из обоих методов, поместите его общую версию в ба- зовый класс и создайте два специализированных метода в подклассах. Вы также можете выделить общий код в отдельный метод и вызвать его из двух первона- чальных методов. В результате программа станет компактнее. Изменять ее станет проще, так как в случае чего вам нужно будет изменить только один метод. Код станет надежнее, потому что для его проверки нужно будет проанализировать только один фрагмент. Изменения будут реже приводить к ошибкам, поскольку вы не сможете по невнимательности внести в идентичные фрагменты програм- мы чуть различающиеся изменения. ГЛАВА 7 Высококачественные методы 161 Поддержка наследования Переопределить небольшой грамотно организован- ный метод легче, чем длинный и плохо спроектированный. Кроме того, стремле- ние к простоте переопределяемых методов уменьшает вероятность ошибок при реализации подклассов. Сокрытие очередности действий Скрывать очередность обработки событий — разумная идея. Например, если программа обычно сначала вызывает метод, запрашивающий информацию у пользователя, а после этого — метод, читающий вспомогательные данные из файла, никакой из этих двух методов не должен за- висеть от порядка их выполнения. В качестве другого примера можно привести две строки кода, первая из которых читает верхний элемент стека, а вторая умень- шает переменную stackTop. Вместо того чтобы распространять такой код по всей системе, скройте предположение о необходимом порядке выполнения двух опе- раций, поместив две эти строки в метод PopStack(). Сокрытие операций над указателями Операции над указателями не отли- чаются удобочитаемостью и часто являются источником ошибок. Изолировав та- кие операции в методах, вы сможете сосредоточиться на их сути, а не на меха- низме манипуляций над указателями. Кроме того, выполнение операций над ука- зателями в одном месте облегчает проверку правильности кода. Если же вы най- дете более эффективный тип данных, чем указатели, изменения затронут лишь несколько методов. Улучшение портируемости Использование методов изолирует непортируе- мый код, явно определяя фрагменты, которые придется изменить при портиро- вании приложения. В число непортируемых аспектов входят нестандартные воз- можности языка, зависимости от оборудования и операционной системы и т. д. Упрощение сложных булевых проверок Понимание сложных булевых проверок редко требуется для понимания пути выполнения программы. Поместив такую про- верку в метод, вы сможете упростить код, потому что (1) детали проверки будут скрыты и (2) описательное имя метода позволит лучше охарактеризовать суть проверки. Создание отдельного метода для проверки подчеркивает ее значимость. Это мо- тивирует программистов сделать детали проверки внутри метода более удобочи- таемыми. В результате и основной путь выполнения кода, и сама проверка стано- вятся более понятными. Упрощение булевых проверок является примером сни- жения сложности, которого мы уже не раз касались. Повышение быстродействия Методы позволяют выполнять оптимизацию кода в одном месте, а не в нескольких. Они облегчают профилирование кода, направ- ленное на определение неэффективных фрагментов. Если код централизован в методе, его оптимизация повысит быстродействие всех фрагментов, в которых этот метод вызывается как непосредственно, так и косвенно, а реализация метода на более эффективном языке или с применением улучшенного алгоритма окажется более выгодной. Для уменьшения объема других методов? Нет. При на- личии стольких разумных причин создания методов эта не нужна. На самом деле для решения некоторых задач лучше использовать один крупный метод (об оптимальном размере метода см. раздел 7.4). Перекрестная ссылка О сокры- тии информации см. подраздел «Скрывайте секреты (к вопро- су о сокрытии информации)» раздела 5.3. 162 ЧАСТЬ II Высококачественный код Операция кажется слишком простой, чтобы создавать для нее метод Один из главных ментальных барьеров, препятствующих созданию эф- фективных методов, — нежелание создавать простой метод для простой цели. Создание метода для двух или трех строк кода может показаться пальбой из пушки по воробьям, но опыт свидетельствует о том, что небольшие методы могут быть чрезвычайно полезны. Небольшие методы обеспечивают несколько преимуществ, и одно из них — об- легчение чтения кода. Так, однажды я обнаружил следующую строку примерно в десятке мест программы: Пример вычисления (псевдокод) points = deviceUnits * ( POINTS_PER_INCH / DeviceUnitsPerInch() ) Наверняка это не самая сложная строка кода в вашей жизни. Большинство людей в итоге поняло бы, что она преобразует некоторую величину, выраженную в ап- паратных единицах, в соответствующее число точек, а кроме того, что каждая из десятка строк делает одно и то же. Однако эти фрагменты можно было сделать еще более ясными, поэтому я создал метод с выразительным именем, выполняю- щий преобразование в одном месте: Пример вычисления, преобразованного в функцию (псевдокод) Function DeviceUnitsToPoints ( deviceUnits Integer ): Integer DeviceUnitsToPoints = deviceUnits * ( POINTS_PER_INCH / DeviceUnitsPerInch() ) End Function В результате все десять первоначальных фрагментов стали выглядеть примерно так: Пример вызова функции (псевдокод) points = DeviceUnitsToPoints( deviceUnits ) Эта строка более понятна и даже кажется очевидной. Данный пример позволяет назвать еще одну причину создания отдельных мето- дов для простых операций: дело в том, что простые операции имеют свойство усложняться с течением времени. После того как я написал метод DeviceUnits- Perlnch(), оказалось, что в определенных условиях при активности определенных устройств он возвращает 0. Для предотвращения деления на 0 мне пришлось на- писать еще три строки кода: Пример кода, расширяющегося при сопровождении программы (псевдокод) Function DeviceUnitsToPoints( deviceUnits: Integer ) Integer; if ( DeviceUnitsPerInch() <> 0 ) DeviceUnitsToPoints = deviceUnits * ( POINTS_PER_INCH / DeviceUnitsPerInch() ) |