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

Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по


Скачать 5.88 Mb.
НазваниеРуководство по стилю программирования и конструированию по
АнкорСовершенный код
Дата31.03.2023
Размер5.88 Mb.
Формат файлаpdf
Имя файлаСовершенный код. Мастер-класс. Стив Макконнелл.pdf
ТипРуководство
#1028502
страница23 из 106
1   ...   19   20   21   22   23   24   25   26   ...   106
ГЛАВА 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() )

ГЛАВА 7 Высококачественные методы
163
else
DeviceUnitsToPoints = 0
end if
End Function
Если бы в коде по#прежнему использовалась первоначальная строка, мне пришлось бы повторить проверку десять раз, добавив в общей сложности 30 строк кода.
Создание простого метода позволило уменьшить это число до 3.
Резюме причин создания методов
Вот список разумных причин создания методов:

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

формирование понятной промежуточной абстракции;

предотвращение дублирования кода;

поддержка наследования;

сокрытие очередности действий;

сокрытие операций над указателями;

улучшение портируемости;

упрощение сложных булевых проверок;

повышение быстродействия.
Кроме того, разумными причинами создания методов можно считать многие из причин создания классов:

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

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

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

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

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

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

выполнение специфического вида рефакторинга.
7.2. Проектирование на уровне методов
Идею связности впервые представили Уэйн Стивенс, Гленфорд Майерс и Ларри
Константайн (Stevens, Myers, and Constantine, 1974). На уровне проектирования классов ее практически вытеснили более современные концепции, такие как аб#
стракция и инкапсуляция, однако на уровне проектирования отдельных методов эвристический принцип связности по#прежнему полезен.
В случае методов связность характеризует соответствие выполняемых в методе операций единой цели. Некоторые программисты предпочитают использовать термин «сила»
(strength): насколько сильно связаны операции в методе? На#
пример, метод
Cosine() (косинус) имеет одну четко опреде#
ленную цель и потому обладает прекрасной связностью. Метод
CosineAndTan()
(косинус и тангенс) имеет меньшую связность, потому что он выполняет сразу
Перекрестная ссылка О связно- сти см. подраздел «Стремитесь к максимальной связности»
раздела 5.3.

164
ЧАСТЬ II Высококачественный код две функции. Наша цель в том, чтобы каждый метод эффективно решал одну за#
дачу и больше ничего не делал.
Вознаграждением будет более высокая надежность кода. В одном иссле#
довании 450 методов было обнаружено, что дефекты отсутствовали в 50%
методов, обладающих высокой связностью, и только в 18% методов с низкой связностью (Card, Church, and Agresti, 1986). Другое исследование 450
методов (это просто совпадение, хотя и весьма необычное) показало, что в срав#
нении с методами, имеющими самое низкое отношение «сопряжение/связность»
(coupling#to#cohesion), методы с максимальным отношением «сопряжение/связ#
ность» содержали в 7 раз больше ошибок, а исправление этих методов было в 20
раз более дорогим (Selby and Basili, 1991).
Обсуждение связности обычно касается нескольких ее уровней. Понять эти кон#
цепции важнее, чем запомнить специфические термины. Используйте концепции как средства, помогающие сделать методы максимально связными.
Функциональная связность — самый сильный и лучший вид связности; она име#
ет место, когда метод выполняет одну и только одну операцию. Примерами мето#
дов, обладающих высокой связностью, являются методы
sin() (синус), GetCusto%
merName() (получить фамилию заказчика), EraseFile() (удалить файл), Calculate%
LoanPayment() (вычислить плату за кредит) и AgeFromBirthdate() (определить воз#
раст по дате рождения). Конечно, такая оценка связности предполагает, что эти методы соответствуют своим именам — иначе они имеют неудачные имена, а об их связности нельзя сказать ничего определенного.
Ниже описаны другие виды связности, которые обычно считаются менее эффек#
тивными.

Последовательная связность (sequential cohesion) наблюдается в том случае,
когда метод содержит операции, которые обязательно выполняются в опре#
деленном порядке, используют данные предыдущих этапов и не формируют в целом единую функцию.
Примером метода с последовательной связностью является метод, вычисляю#
щий по дате рождения возраст сотрудника и срок до его ухода на пенсию. Если метод вычисляет возраст и затем использует этот результат для нахождения срока до ухода сотрудника на пенсию, он имеет последовательную связность.
Если метод находит возраст сотрудника, после чего в абсолютно другом вы#
числении определяет срок до ухода на пенсию, применяя те же данные о дате рождения, он имеет только коммуникационную связность.
Как сделать такой метод функционально связным? Создать два отдельных ме#
тода: метод, вычисляющий по дате рождения возраст сотрудника, и метод,
определяющий по дате рождения срок до ухода сотрудника на пенсию. Вто#
рой метод мог бы вызывать метод нахождения возраста. Оба этих метода име#
ли бы функциональную связность. Другие методы могли бы вызывать любой из них или оба.

Коммуникационная связность (communicational cohesion) имеет место, когда вы#
полняемые в методе операции используют одни и те же данные и не связаны между собой иным образом. Если метод печатает отчет, после чего заново инициализи#

ГЛАВА 7 Высококачественные методы
165
рует переданные в него данные, он имеет коммуникационную связность: две опе#
рации объединяет только то, что они обращаются к одним и тем же данным.
Чтобы повысить связность этого метода, выполняйте повторную инициализа#
цию данных около места их создания, которое не должно находиться в мето#
де печати отчета. Разделите операции на два метода: первый будет печатать отчет, а второй — выполнять повторную инициализацию данных неподалеку от кода, создающего или изменяющего данные. Вызовите оба этих метода вместо первоначального метода, имевшего коммуникационную связность.

Временная связность (temporal cohesion) наблюдается, когда операции объе#
диняются в метод на том основании, что все они выполняются в один интер#
вал времени. Типичные примеры — методы
Startup() (запуск программы) Comp%
leteNewEmployee() (прием нового сотрудника на работу) и Shutdown() (завер#
шение программы). Временную связность порой считают неприемлемой, по#
скольку иногда она связана с плохими методиками программирования, таки#
ми как включение слишком разнообразного кода в метод
Startup().
Для устранения этой проблемы рассматривайте методы с временной связнос#
тью как способы организации других событий. Так, метод
Startup() мог бы читать конфигурационный файл, инициализировать вспомогательный файл, настра#
ивать менеджер памяти и выводить первоначальное окно программы. Чтобы сделать метод с временной связностью максимально эффективным, не выпол#
няйте в нем конкретных операций непосредственно, а вызывайте для их вы#
полнения другие методы. Тогда всем будет ясно, что суть метода — согласова#
ние действий, а не их выполнение.
Этот пример поднимает вопрос выбора имени, описывающего такой метод с адекватным уровнем абстракции. Вы могли бы назвать метод
ReadConfigFileIn%
itScratchFileEtc() (прочитать конфигурационный файл, инициализировать вспо#
могательный файл и т. д.), но из этого следовало бы, что он имеет только слу#
чайную связность. Если же вы назовете метод
Startup(), будет очевидно, что он имеет одну цель и поэтому обладает функциональной связностью.
Остальные виды связности обычно неприемлемы. Они приводят к созданию плохо организованного кода, который трудно отлаживать и изменять. Метод с плохой связностью лучше переписать, чем тратить время и средства на поиск проблем.
Однако знание того, чего следует избегать, может пригодиться, поэтому ниже я привел описания плохих видов связности.

Процедурная связность (procedural cohesion) имеет место, когда операции в методе выполняются в определенном порядке. В качестве примера можно при#
вести метод, получающий фамилию сотрудника, затем его адрес, а после это#
го номер телефона. Порядок этих операций важен только потому, что он со#
ответствует порядку, в котором пользователя просят ввести данные. Остальные данные о сотруднике получает другой метод. В данном случае операции вы#
полняются в определенном порядке и не объединены больше ничем, поэтому метод имеет процедурную связность.
Для достижения лучшей связности поместите разные операции в отдельные методы. Сделайте так, чтобы вызывающий метод решал одну задачу, причем пол#
ностью: пусть он соответствует имени
GetEmployee() (получить данные о со#

166
ЧАСТЬ II Высококачественный код труднике), а не
GetFirstPartOfEmployeeData() (получить первую часть данных о сотруднике). Вероятно, при этом придется изменить и методы, получающие остальные данные. Довольно часто достижение функциональной связности требует изменения двух или более первоначальных методов.

Логическая связность (logical cohesion) имеет место, когда метод включает несколько операций, а выбор выполняемой операции осуществляется на ос#
нове передаваемого в метод управляющего флага. Этот вид связности называ#
ется логическим потому, что операции метода объединены только управляю#
щей «логикой» метода: крупным оператором
if или рядом блоков case. Какой#
нибудь другой по#настоящему «логической» связи между операциями нет. По#
скольку определяющим атрибутом логической связности является отсутствие отношений между операциями, возможно, лучше было бы назвать ее «нелогич#
ной связностью».
В качестве примера такого метода можно привести метод
InputAll(), принима#
ющий в зависимости от полученного флага фамилии клиентов, данные карт учета времени сотрудников или инвентаризационные данные. Другие приме#
ры — методы
ComputeAll(), EditAll(), PrintAll() и SaveAll(). Главная проблема с ними в том, что передавать флаг для управления работой метода нецелесообразно.
Вместо метода, выполняющего одну из трех операций в зависимости от полу#
ченного флага, лучше создать три метода, выполняющих по одной операции.
Если операции используют некоторый одинаковый код или общие данные, код следует переместить в метод более низкого уровня, а методы упаковать в класс.
Однако логически связный метод вполне приемлем, если его код состоит исключительно из ряда операторов
if или case
и вызовов других методов. Если единственная роль метода
— координация выполнения команд и сам он не выполня#
ет действий, это обычно удачное проектное решение. Такие методы еще называют «обработчиками событий». Обработ#
чики часто используются в интерактивных средах, таких как
Apple Macintosh, Microsoft Windows и других средах с GUI.

При
случайной связности (coincidental cohesion) каких#
либо ясных отношений между выполняемыми в методе опе#
рациями нет. Этот вариант можно еще называть «отсутстви#
ем связности» или «хаотичной связностью». Низкокачествен#
ный метод C++, приведенный в начале этой главы, имеет случайную связность.
Случайную связность трудно преобразовать в более приемлемый вид связнос#
ти — как правило, методы со случайной связностью нужно проектировать и реализовать заново.
Никакой из этих терминов не является магическим или священным. Изу#
чайте идеи, а не терминологию. Стремитесь создавать методы с функци#
ональной связностью — это возможно почти всегда.
Перекрестная ссылка Связность такого метода может быть удов- летворительной, однако при этом возникает один вопрос проектирования более высоко- го уровня: использовать ли опе- раторы case вместо полиморф- ного метода? См. также подраз- дел «Замена условных операто- ров (особенно многочисленных блоков case) на вызов полимор- фного метода» раздела 24.3.

ГЛАВА 7 Высококачественные методы
167
7.3. Удачные имена методов
Имя метода должно ясно описывать все, что он делает. Со#
веты по выбору удачных имен методов приведены ниже.
Описывайте все, что метод выполняет Опишите в имени метода все выходные данные и все побочные эффекты. Если метод вычис#
ляет сумму показателей в отчете и открывает выходной файл, имя
ComputeReport%
Totals() не будет адекватным. ComputeReportTotalsAndOpenOutputFile() — имя адек#
ватное, но слишком длинное и несуразное. Создавая методы с побочными эффек#
тами, вы получите много длинных несуразных имен. Выход из этого положения
— не использование менее описательных имен, а создание ясных методов без по#
бочных эффектов.
Избегайте невыразительных и неоднозначных глаголов Некоторые глаголы могут описывать практически любое действие. Имена вроде
HandleCalculation(),
PerformServices(), OutputUser(), ProcessInput() и DealWithOutput() не говорят о ра#
боте методов почти ничего. В лучшем случае по этим именам можно догадаться,
что методы имеют какое#то отношение к вычислениям, сервисам, пользователям,
вводу и выводу соответственно. Исключением было бы использование глагола
«handle» в специфическом техническом смысле обработки события.
Иногда единственным недостатком метода является невыразительность его имени; сам метод при этом может быть спроектирован очень хоро#
шо. Если имя
HandleOutput() заменить на FormatAndPrintOutput(), роль метода станет очевидной.
В других случаях невыразительность глагола в имени метода может объясняться аналогичным поведением метода. Неясная цель — невыразительное имя. Если это так, лучше всего выполнить реструктуризацию метода и всех родственных мето#
дов, чтобы все они получили более четкие цели и более выразительные имена,
точно их описывающие.
Не используйте для дифференциации имен методов исключитель'
но номера Один разработчик написал весь свой код в форме единствен#
ного объемного метода. Затем он разбил код на фрагменты по 15 строк и создал методы
Part1, Part2 и т. д. После этого он создал один высокоуровневый метод, вызывающий каждую часть кода. Подобный способ создания и именова#
ния методов глуп до невозможности (и столь же редок, надеюсь). И все же про#
граммисты иногда используют номера для дифференциации таких методов, как
OutputUser, OutputUser1 и OutputUser2. Номера в конце каждого из этих имен ни#
чего не говорят о различиях представляемых методами абстракций, поэтому та#
кие имена нельзя признать удачными.
Не ограничивайте длину имен методов искусственными правилами Ис#
следования показывают, что оптимальная длина имени переменной равняется в среднем 9–15 символам. Как правило, методы сложнее переменных, поэтому и адекватные имена методов обычно длиннее. В то же время к именам методов ча#
сто присоединяются имена объектов, что по сути предоставляет методам часть имени «бесплатно». Главной задачей имени метода следует считать как можно более ясное и понятное описание сути метода, поэтому имя может иметь любую длину,
удовлетворяющую этой цели.
Перекрестная ссылка Об имено- вании переменных см. главу 11.

168
ЧАСТЬ II Высококачественный код
Для именования функции используйте описание воз'
вращаемого значения Функция возвращает значение, и это следует должным образом отразить в ее имени. Так,
имена
cos(), customerId.Next(), printer.IsReady() и pen.Current%
Color() ясно указывают, что возвращают функции, и потому являются удачными.
Для именования процедуры используйте выразительный глагол, дополняя
его объектом Процедура с функциональной связностью обычно выполняет опе#
рацию над объектом. Имя должно отражать выполняемое процедурой действие и объект, над которым оно выполняется, что приводит нас к формату «глагол +
объект». Примеры удачных имен процедур —
PrintDocument(),CalcMonthlyRevenues(),
CheckOrderInfo() и RepaginateDocument().
В случае объектно#ориентированных языков имя объекта в имя процедуры вклю#
чать не нужно, потому что объекты и так входят в состав вызовов, принимающих вид
document. Print(), orderInfo. Check() и monthlyRevenues. Calc(). Имена вида docu%
ment . PrintDocument() страдают от избыточности и могут стать в производных классах неверными. Если
Check — класс, производный от класса Document, суть вы#
зова
check. Print() кажется очевидной: печать чека. В то же время вызов check.Print%
Document() похож на печать записи чековой книжки или ежемесячной выписки со счета, но никак не чека.
Дисциплинированно используйте антонимы Приме#
нение конвенций именования, подразумевающих использо#
вание антонимов, поддерживает согласованность имен, что облегчает чтение кода. Антонимы вроде first/last понятны всем. Пары вроде FileOpen() и _lclose() несимметричны и вызывают замешательство. Вот некоторые антонимы, попу#
лярные в программировании:
add/remove increment/decrement open/close begin/end insert/delete show/hide create/destroy lock/unlock source/target first/last min/max start/stop get/put next/previous up/down get/set old/new
Определяйте конвенции именования часто используемых операций При работе над некоторыми системами важно различать разные виды операций. Са#
мым легким и надежным способом определения этих различий часто оказывает#
ся конвенция именования.
В одном из моих проектов каждый объект имел уникальный идентификатор. Мы не потрудились выработать конвенцию именования методов, возвращающих иден#
тификатор объекта, и в итоге получили такие имена, как:
employee.id.Get()
dependent.GetId()
supervisor()
candidate.id()
Класс
Employee предоставлял доступ к объекту id, который в свою очередь вклю#
чал метод
Get(). Класс Dependent предоставлял для этой цели метод GetId(). Разра#
Перекрестная ссылка О разли- чии между процедурами и функ- циями см. раздел 7.6.
Перекрестная ссылка Похожий список антонимов, используе- мых в именах переменных, см.
в подразделе «Антонимы, час- то встречающиеся в именах переменных» раздела 11.1.

1   ...   19   20   21   22   23   24   25   26   ...   106


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