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

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


Скачать 7.6 Mb.
НазваниеРуководство по стилю программирования и конструированию по
Дата18.05.2023
Размер7.6 Mb.
Формат файлаpdf
Имя файлаCode_Complete.pdf
ТипРуководство
#1139697
страница21 из 104
1   ...   17   18   19   20   21   22   23   24   ...   104
ГЛАВА 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() )

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


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