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

Создание, анализ ирефакторинг


Скачать 3.16 Mb.
НазваниеСоздание, анализ ирефакторинг
Дата29.09.2022
Размер3.16 Mb.
Формат файлаpdf
Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
ТипКнига
#706087
страница37 из 49
1   ...   33   34   35   36   37   38   39   40   ...   49
employees) {
for (HourlyEmployee e : employees) {
addLineItemToPage(e);
if (page.size() == PAGE_SIZE)
printAndClearItemList();
}
if (page.size() > 0)
printAndClearItemList();
}
private void printAndClearItemList() {
formatter.format(page);
page.clear();
}
private void addLineItemToPage(HourlyEmployee e) {
LineItem item = new LineItem();
item.name = e.getName();
item.hours = e.getTenthsWorked() / 10;
item.tenths = e.getTenthsWorked() % 10;
page.add(item);
}
public class LineItem {
public String name;
public int hours;
public int tenths;
}
}
У этого кода имеется логическая зависимость, лишенная физического вопло- щения . Удастся ли вам найти ее? Речь идет о константе
PAGE_SIZE
. Почему класс
HourlyReporter должен знать размер страницы? За размер страницы должен от- вечать класс
HourlyReportFormatter
Факт объявления
PAGE_SIZE
в
HourlyReporter указывает на неверное размещение
[G17] . Разработчик полагает, что классу
HourlyReporter необходимо знать размер страницы . Такое предположение является логической зависимостью . Работа
HourlyReporter зависит от того факта, что
HourlyReportFormatter поддерживает раз- мер страницы до 55 . Если какая-то реализация
HourlyReportFormatter не сможет работать с такими страницами, произойдет ошибка .
337

338
Глава 17 . Запахи и эвристические правила
Чтобы создать физическое представление этой зависимости, мы можем включить в
HourlyReportFormatter новый метод с именем getMaxPageSize()
. В дальнейшем
HourlyReporter вызывает эту функцию вместо того, чтобы использовать константу
PAGE_SIZE
g23: Используйте полиморфизм
вместо if/else или switch/Case
Эта рекомендация может показаться странной, если вспомнить главу 6 . Ведь в этой главе говорилось, что в тех частях системы, в которые с большей веро- ятностью будут добавляться новые функции, а не новые типы, команды switch вполне уместны .
Во-первых, команды switch чаще всего используются только потому, что они представляют очевидное решение методом «грубой силы», а не самое уместное решение для конкретной ситуации . Таким образом, это эвристическое правило напоминает нам о том, что до применения switch следует рассмотреть возмож- ность применения полиморфизма .
Во-вторых, ситуации, в которых состав функций менее стабилен, чем состав типов, встречаются относительно редко . Следовательно, к каждой конструкции switch следует относиться с подозрением .
Я использую правило «ОДНОЙ КОМАНДЫ SWITCH»: для каждого типа выбо-
ра программа не должна содержать более одной команды switch. Множественные
конструкции switch следует заменять полиморфными объектами.
g24: Соблюдайте стандартные конвенции
Все рабочие группы должны соблюдать единые стандарты ко дирования, основан- ные на отраслевых нормах . Стандарт кодирования опреде ляет, где объявляются переменные экземпляров; как присваиваются имена классов, методов и пере- менных; где размещаются фигурные скобки и т . д . Документ с явным описанием этих правил не нужен — сам код служит примером оформления .
Правила должны соблюдаться всеми участниками группы . Это означает, что каждый участник группы должен быть достаточно разумным, чтобы понимать: неважно, как именно размещаются фигурные скобки, если только все согласились размещать их одинаковым образом .
Если вам захочется узнать, какие конвенции оформления использую я, обрати- тесь к переработанному коду в листингах Б .7 (с . 442) — Б .14 .
338

Разное
339
g25: Заменяйте «волшебные числа»
именованными константами
Вероятно, это одно из самых древних правил разработки . Помню, оно встречалось мне еще в 60-х годах, в учебниках COBOL, FORTRAN и PL/1 для начинающих .
В общем случае присутствие «сырых» чисел в коде нежелательно . Числа следует скрыть в константах с содержательными именами .
Например, число 86,400 следует скрыть в константе
SECONDS_PER_DAY
. Если в стра- нице отчета выводится 55 строк, число 55 следует скрыть в константе
LINES_PER_
PAGE
Некоторые числа так легко узнаются, что их не обязательно скрывать за име- нованными константами — при условии, что они используются в сочетании с предельно ясным кодом . Пример:
double milesWalked = feetWalked/5280.0;
int dailyPay = hourlyRate * 8;
double circumference = radius * Math.PI * 2;
Нужны ли константы
FEET_PER_MILE
,
WORK_HOURS_PER_DAY
и
TWO
в этих примерах?
Разумеется, последний случай выглядит особенно абсурдно . В некоторых фор- мулах константы попросту лучше воспринимаются в числовой записи . По поводу
WORK_HOURS_PER_DAY
можно спорить, потому что законы и нормативы могут изме- няться . С другой стороны, формула с числом 8 читается настолько удобно, что мне просто не хочется нагружать читателя кода лишними 17 символами . А число
5280 — количество футов в миле — настолько хорошо известно и уникально, что читатель сразу узнает его, даже если оно будет располагаться вне какого-либо контекста .
Такие константы, как 3 .141592653589793, тоже хорошо известны и легко узнавае- мы . Однако вероятность ошибки слишком велика, чтобы оставлять их в числовой форме . Встречая значение 3 .1415927535890793, вы сразу догадываетесь, что перед вами число
π, и не проверяете его (а вы заметили ошибку в одной цифре?) . Также мы не хотим, чтобы в программах использовались сокращения 3 .14, 3 .14159, 3 .142 и т . д . К счастью, значение
Math.PI
уже определено за нас .
Термин «волшебное число» относится не только к числам . Он распространяется на все лексемы, значения которых не являются самодокументирующими . При- мер:
assertEquals(7777, Employee.find("John Doe").employeeNumber());
В этом проверочном условии задействованы два «волшебных числа» . Очевидно, первое — 7777, хотя его смысл далеко не так очевиден . Второе «волшебное чис- ло» — строка "
John Doe
"
. Ее смысл тоже выглядит весьма загадочно .
Оказывается,
"
John Doe
"
— имя работника с табельным номером 7777 в тестовой базе данных, созданной нашей группой . Все участники группы знают, как под-
339

340
Глава 17 . Запахи и эвристические правила ключаться к этой базе данных . В базе уже хранятся тестовые записи с заранее известными значениями и атрибутами . Также выясняется, что "
John Doe
"
— един- ственный работник с почасовой оплатой в тестовой базе данных . Следовательно, эта проверка должна выглядеть так:
assertEquals(
HOURLY_EMPLOYEE_ID,
Employee.find(HOURLY_EMPLOYEE_NAME).employeeNumber());
g26: Будьте точны
Наивно ожидать, что первая запись, возвращаемая по запросу, является единственной . Использовать числа c плавающей точкой для представления де нежных сумм — почти преступление . Отсутствие блокировок и/или управ- ления транз акциями только потому, что вы думаете, что одновременное обнов- ление маловероятно — в лучшем случае халатность . Объявление переменной с типом
ArrayList там, где более уместен тип
List
— чрезмерное ограничение .
Объявление всех переменных защищенными по умолчанию — недостаточное ограничение .
Принимая решение в своем коде, убедитесь в том, что вы действуете предельно точно и аккуратно . Знайте, почему принимается решение, и как вы собираетесь поступать с исключениями из правила . Не ленитесь обеспечивать точность своих решений . Если вы решили вызвать функцию, которая может вернуть null
— про- верьте возвращаемое значение . Если вы запрашиваете из базы данных запись, которая, по вашему мнению, является единственной — проверьте, не вернул ли запрос дополнительные записи . Если вам нужно работать с денежными суммами, используйте целые числа и округляйте результат по действующим правилам .
Если в программе существует возможность одновременного объявления, реали- зуйте ту или иную разновидность блокировки . Неоднозначности и неточности в коде объясняются либо недопониманием, либо ленью . В любом случае от них следует избавиться .
g27: Структура важнее конвенций
Воплощайте архитектурные решения на уровне структуры кода; она важнее стандартов и конвенций . Содержательные имена полезны, но структура, застав- ляющая пользователя соблюдать установленные правила, важнее . Например, конструкции switch
/
case с хорошо выбранными именами элементов перечисле- ния уступают базовым классам с абстрактными методами . Ничто не вынуждает пользователя применять одинаковую реализацию switch
/
case во всех случаях; с другой стороны, базовые классы заставляют его реализовать все абстрактные методы в конкретных классах .
340

Разное
341
g28: Инкапсулируйте условные конструкции
В булевской логике достаточно трудно разобраться и вне контекста команд if или while
. Выделите в программе функции, объясняющие намерения условной конструкции . Например, команда if (shouldBeDeleted(timer))
выразительнее команды if (timer.hasExpired() && !timer.isRecurrent())
g29: Избегайте отрицательных условий
Отрицательные условия немного сложнее для понимания, чем положительные .
Таким образом, по возможности старайтесь формулировать положительные условия . Например, запись if (buffer.shouldCompact())
предпочтительнее записи if (!buffer.shouldNotCompact())
g30: Функции должны выполнять одну операцию
Часто возникает искушение разделить свою функцию на несколько секций для выполнения разных операций . Такие функции выполняют несколько операций; их следует преобразовать в группу меньших функций, каждая из которых вы- полняет только одну операцию .
Пример:
public void pay() {
for (Employee e : employees) {
if (e.isPayday()) {
Money pay = e.calculatePay();
e.deliverPay(pay);
}
}
}
Эта функция выполняет сразу три операции: она перебирает всех работников; проверяет, начислены ли работнику какие-то выплаты; и наконец, производит оплату . Код лучше записать в следующем виде:
public void pay() {
for (Employee e : employees)
payIfNecessary(e);
}
private void payIfNecessary(Employee e) {
if (e.isPayday())
calculateAndDeliverPay(e);
}
341

342
Глава 17 . Запахи и эвристические правила private void calculateAndDeliverPay(Employee e) {
Money pay = e.calculatePay();
e.deliverPay(pay);
}
Каждая из этих функций выполняет только одну операцию (см . «Правило одной операции», с . 59) .
g31: Скрытые временн
*ые привязки
Временн
*ые привязки часто необходимы, но они не должны скрываться . Струк- тура аргументов функций должна быть такой, чтобы последовательность вызова была абсолютно очевидной . Рассмотрим следующий пример:
public class MoogDiver {
Gradient gradient;
List splines;
public void dive(String reason) {
saturateGradient();
reticulateSplines();
diveForMoog(reason);
}
}
Порядок вызова трех функций важен . Сначала вызывается saturateGradient()
, затем reticulateSplines()
и только после этого diveForMoog()
. К сожалению, код не обеспечивает принудительного соблюдения временной привязки . Ничто не мешает другому программисту вызвать reticulateSplines до saturateGradient
, и все кончится исключением
UnsaturatedGradientException
Более правильное решение выглядит так:
public class MoogDiver {
Gradient gradient;
List splines;
public void dive(String reason) {
Gradient gradient = saturateGradient();
List splines = reticulateSplines(gradient);
diveForMoog(splines, reason);
}
}
Временная привязка реализуется посредством создания «эстафеты» . Каждая функция выдает результат, необходимый для работы следующей функции, и вы- звать эти функции с нарушением порядка сколько-нибудь разумным способом уже не удастся .
Пожалуй, кто-то сочтет, что это увеличивает сложность функций, и это действи- тельно так . Однако дополнительные синтаксические сложности лишь выявляют
342

Разное
343
реальную сложность ситуации, обусловленную необходимостью согласования по времени .
Обратите внимание: переменные экземпляров остались на своих местах . Пред- полагается, что они используются приватными методами класса . Использование их в аргументах лишь явно выражает факт существования временной привязки .
g32: Структура кода должна быть обоснована
Структура кода должна выбираться не произвольно, а по строго определенным причинам . Позаботьтесь о том, чтобы эти причины были выражены в структуре кода . Если при чтении кода создается впечатление, что его структура выбрана произвольно, другим пользователям может показаться, что ее можно изменить .
Если во всей системе последовательно используется единая структура кода, другие пользователи примут ее и сохранят действующие правила . Например, недавно я занимался объединением изменений в FitNesse и обнаружил, что один из наших авторов использовал следующую запись:
public class AliasLinkWidget extends ParentWidget
{
public static class VariableExpandingWidgetRoot {
}
Проблема в том, что
VariableExpandingWidgetRoot незачем находиться в облсти видимости
AliasLinkWidget
. Более того, класс
AliasLinkWidget
VariableExpanding-
WidgetRoot использовался посторонними классами, которые не имели никакого отношения к
AliasLinkWidget
Возможно, программист разместил
VariableExpandingWidgetRoot в
AliasWidget по со- ображениям удобства, а может, он действительно полагал, что область видимости этого класса должна находиться внутри области видимости
AliasWidget
. Какими бы причинами он ни руководствовался, результат выглядит необоснованным .
Открытые классы, не являющиеся вспомогательными по отношению к другому классу (то есть используемыми только в его внутренних операциях), не должны размещаться внутри других классов . По стандартным правилам такие классы объявляются на верхнем уровне своих пакетов .
g33: Инкапсулируйте граничные условия
Отслеживать граничные условия нелегко . Разместите их обработку в одном ме- сте . Не позволяйте им «растекаться» по всему коду . Не допускайте, чтобы в вашей программе кишели многочисленные
+1
и
–1
. Возьмем простой пример из FIT:
if(level + 1 < tags.length)
{
parts = new Parse(body, tags, level + 1, offset + endTag);
343

344
Глава 17 . Запахи и эвристические правила body = null;
}
Обратите внимание: level+1
здесь встречается дважды . Это граничное усло- вие, которое следует инкапсулировать в переменной — например, с именем nextLevel
:
int nextLevel = level + 1;
if(nextLevel < tags.length)
{
parts = new Parse(body, tags, nextLevel, offset + endTag);
body = null;
}
g34: Функции должны быть написаны
на одном уровне абстракции
Все команды функции должны быть сформулированы на одном уровне абстрак- ции, который расположен одним уровнем ниже операции, описываемой именем функции . Возможно, это эвристическое правило сложнее всего правильно ин- терпретировать и соблюдать . Идея достаточно тривиальна, но люди слишком хорошо справляются со смешением разных уровней абстракции . Для примера возьмем следующий код из FitNesse:
public String render() throws Exception
{
StringBuffer html = new StringBuffer("
1   ...   33   34   35   36   37   38   39   40   ...   49


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