Файл: Руководство по стилю программирования и конструированию по.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.11.2023
Просмотров: 766
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
ГЛАВА 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
ГЛАВА 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) имеет место, когда вы- полняемые в методе операции используют одни и те же данные и не связаны между собой иным образом. Если метод печатает отчет, после чего заново инициализи-