Один уровень абстракции на функцию
Чтобы убедиться в том, что функция выполняет «только одну операцию», необходимо проверить, что все команды функции находятся на одном уровне абстракции. Легко убедиться, что листинг 3.1 нарушает это правило. Некоторые из его концепций — например, getHtml() — находятся на очень высоком уровне абстракции; другие (скажем, String pagePathName = PathParser.render(pagePath)) — на среднем уровне. Наконец, третьи — такие, как .append("\n") — относятся к чрезвычайно низкому уровню абстракции.
Смешение уровней абстракции внутри функции всегда создает путаницу. Читатель не всегда понимает, является ли некоторое выражение важной концепцией или второстепенной подробностью. Что еще хуже, при их смешении функция постепенно начинает обрастать все большим количеством второстепенных подробностей.
Чтение кода сверху вниз: правило понижения
Код должен читаться как рассказ — сверху вниз [KP78, p. 37].
За каждой функцией должны следовать функции следующего уровня абстракции. Это позволяет читать код, последовательно спускаясь по уровням абстракции в ходе чтения списка функций. Я называю такой подход «правилом понижения».
Сказанное можно сформулировать и иначе: программа должна читаться так, словно она является набором TO-абзацев, каждый из которых описывает текущий уровень абстракции и ссылается на последующие TO-абзацы следующего нижнего уровня.
• Чтобы включить начальные и конечные блоки, мы сначала включаем начальные блоки, затем содержимое тестовой страницы, а затем включаем конечные блоки.
• Чтобы включить начальные блоки, мы сначала включаем пакетные начальные блоки, если имеем дело с пакетом тестов, а затем включаем обычные начальные блоки.
• Чтобы включить пакетные начальные блоки, мы ищем в родительской иерархии страницу SuiteSetUp и добавляем команду include с путем к этой странице.
• Чтобы найти в родительской иерархии…
Опыт показывает, что программистов очень трудно научить следовать этому правилу и писать функции, остающиеся на одном уровне абстракции. Тем не менее освоить этот прием очень важно. Он играет ключевую роль для создания коротких функций, выполняющих только одну операцию. Построение кода по аналогии с набором последовательных TO-абзацев — эффективный метод поддержания единого уровня абстракции.
Взгляните на листинг 3.7 в конце этой главы. В нем приведен полный код функции testableHtml, переработанной в соответствии с описанными здесь принципами. Обратите внимание на то, как каждая функция «представляет» читателю следующую функцию и как каждая функция остается на едином уровне абстракции.
Команды switch
Написать компактную команду switch довольно сложно[14]. Даже команда switch всего с двумя условиями занимает больше места, чем в моем представлении должен занимать один блок или функция. Также трудно создать команду switch, которая делает что-то одно — по своей природе команды switch всегда выполняют N операций. К сожалению, обойтись без команд switch удается не всегда, но по крайней мере мы можем позаботиться о том, чтобы эти команды были скрыты в низкоуровневом классе и не дублировались в коде. И конечно, в этом нам может помочь полиморфизм.
В листинге 3.4 представлена всего одна операция, зависящая от типа работника.
public Money calculatePay(Employee e)
throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
Эта функция имеет ряд недостатков. Во-первых, она велика, а при добавлении новых типов работников она будет разрастаться. Во-вторых, она совершенно очевидно выполняет более одной операции. В-третьих, она нарушает принцип единой ответственности[15], так как у нее существует несколько возможных причин изменения. В-четвертых, она нарушает принцип открытости/закрытости[16], потому что код функции должен изменяться при каждом добавлении новых типов. Но, пожалуй, самый серьезный недостаток заключается в том, что программа может содержать неограниченное количество других функций с аналогичной структурой, например:
isPayday(Employee e, Date date)
или
deliverPay(Employee e, Money pay)
и так далее. Все эти функции будут иметь все ту же ущербную структуру.