понедельник, 30 сентября 2013 г.

Как писать функции

Очередная порция мудрости из книги Р. Мартина "Чистый код. Создание, анализ и рефакторинг". Оттуда я выписала не все, а только те правила, которые буду стараться применять в ближайшее время.

1. Функции должны быть компактными. Желательно, чтобы длина функции не превышала 20 строк.

2. Максимальный уровень отступов в функции не должен превышать одного-двух (отступы создаются командами if, else, while и т.д.)

3. Функция должна выполнять только одну операцию. Она должна выполнять ее хорошо и ничего другого она делать не должна. Чтобы убедиться в том, что функция "выполняет только одну операцию", необходимо проверить, что все команды функции находятся на одном уровне абстракции. Например, посмотрим на вот такой кусочек кода:


В этом куске кода данное правило нарушается. Некоторые из концепций - например, getHtml() - находятся на очень высоком уровне абстракции, другие (например, String pagePathName = PathParser.render(pagePath) - на среднем уровне). Наконец, третьи - такие, как .append("\n") - относятся к чрезвычайно низкому уровню абстракции.

Смешение уровней абстракции внутри функции всегда создает путаницу. Читатель не всегда понимает, является ли некоторое выражение важной концепцией или второстепенной подробностью. Что еще хуже, при их смешении функция постепенно начинает обрастать все большим количеством второстепенных подробностей.

4. Код должен читаться как рассказ - сверху вниз. За каждой функцией должны следовать функции следующего уровня абстракции. Это позволяет читать код, последовательно спускаясь по уровням абстракции в ходе чтения списка функций.

5. В идеальном случае количество аргументов функции равно нулю. Функций с тремя аргументами следует по возможности избегать. Аргументы не только ухудшают читаемость кода, но и создают больше проблем с точки зрения тестирования. Только представьте, как трудно составить все тестовые сценарии, проверяющие правильность работы кода со всеми комбинациями аргументов.

6. Если функция должна получать более двух или трех аргументов, весьма вероятно, что некоторые из этих аргументов  стоит упаковать в отдельном классе. Рассмотрим следующие два объявления:

Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius)

Сокращение количества аргументов посредством создания объектов может показаться жульничеством, но это не так. Если переменные передаются совместно как единое целое (как переменные x и y в этом примере), то, скорее всего, вместе они образуют концепцию, заслуживающую собственного имени.

7. Выбор хорошего имени для функции  способен в значительной мере объяснить смысл функции, а также порядок и смысл ее аргументов. В унарных функциях сама функция и ее аргумент должны образовывать естественную пару "глагол/существительное". Например, вызов вида write(name) смотрится весьма информативно. Читатель понимает, что чем бы ни было "имя", оно куда-то "записывается". Еще лучше запись writeField(name), которая сообщает, что "имя" записывается в "поле" какой-то структуры.

8. Избавьтесь от побочных эффектов. Ваша функция обещает сделать что-то одно, но делает что-то другое, скрытое от пользователя. Иногда она вносит неожиданные изменения в переменные своего класса - скажем, присваивает им значения параметров, переданных функции или глобальных переменных системы. в любом случае такая функция является коварной и вредоносной. Для примера рассмотрим следующую функцию:

Функция использует стандартный алгоритм для проверки пары "имя пользователя/пароль". Она возвращает true или false. Побочным эффектом этой функции является вызов Session.initialize(). Имя checkPassword сообщает, что функция проверяет пароль. Оно ничего не говорит о том, что функция инициализирует сеанс. Таким образом, тот, кто поверит имени функции, рискует потерять текущие сеансовые данные, когда он решит проверить данные пользователя. 

Побочный эффект создает временную привязку. А именно, функция checkPassword может вызываться только в определенные моменты времени (когда инициализация сеанса может быть выполнена безопасно). Несвоевременный вызов может привести к непреднамеренной потере сеансовых данных. Временные привязки создают массу проблем, особенно когда они прячутся в побочных эффектах. Если без временной привязки не обойтись, этот факт должен быть четко оговорен в имени функции. В нашем примере функцию можно было бы переименовать в checkPasswordAndInitializeSession, хотя это безусловно нарушает правило одной операции.

9. Почти все наверняка сталкивались с необходимостью дополнительной проверки аргументов, которые на самом деле оказывались выходными, а не входными. Пример: appendFooter(s). Присоединяет ли эта функция s в качестве завершающего блока к чему-то другому? Или она присоединяет какой-то завершающий блок к s? Является ли s входным или выходным аргументом? Конечно, можно посмотреть на сигнатуру функции и получить ответ. Но это нарушает естественный ритм чтения кода. На самом деле функцию appendFooter лучше вызывать в виде: 
report.appendFooter()
В общем случае выходных аргументов стоит избегать. Если ваша функция должна изменять чье-то состояние, пусть она изменяет состояние своего объекта-владельца.

10. Функция должна что-то делать или отвечать на какой-то вопрос, но не одновременно. Либо функция изменяет состояние объекта, либо возвращает информацию об этом объекте. Совмещение двух операций часто вызывает путаницу. Для примера рассмотрим следующую функцию:

public boolean set(String attribute, String value);

Функция присваивает значение атрибуту с указанным именем  и возвращает true, если присваивание прошло успешно, или false, если такой атрибут не существует. Это приводит к появлению странных конструкций вида 

if (set ("username", "UncleBob"))...

Представьте происходящее с точки зрения читателя кода. Что проверяет это условие? Что атрибут "username" содержит ранее присвоенное значение "UncleBob"? Или что атрибуту "username" успешно присвоено значение "UncleBob"? Смысл невозможно вывести из самого вызова, потому что мы не знаем,  чем в данном случае является слово set - глаголом или прилагательным. 

Полноценное решение заключается в отделении команды от запроса, чтобы в принципе исключить любую неоднозначность:

if (attributeExists("username")){
    setAttribute("username", "UncleBob")
...
}

11. Изолируйте блоки try/catch. Блоки try/catch выглядят весьма уродливо. Они запутывают структуру кода и смешивают обработку ошибок с нормальной обработкой. По этой причине тела блоков try и catch рекомендуется выделять в отдельные функции:

12. Используйте исключения, а не коды ошибок!

13. Если ваши функции остаются очень компактными, редкие вкрапления множественных return, команд break и continue не принесут вреда, а иногда даже могут повысить выразительность по сравнению с классической реализацией с одной точкой входа и одной точкой выхода. 




Комментариев нет:

Отправить комментарий