понедельник, 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 не принесут вреда, а иногда даже могут повысить выразительность по сравнению с классической реализацией с одной точкой входа и одной точкой выхода. 




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

Как пользоваться Sublime 2

Ниже будут представлены не все тонкости работы с Sublime, а только те, которыми буду пользоваться в ближайшее время. Все проверялось на ОС Ubuntu.

Ctrl + + -  увеличить шрифт
Ctrl + /  -  закомментировать/раскомментировать выделенный текст

Если нажать на Shift и правую кнопку мыши, то можно сделать вертикальное выделение текста:



Если нажать на Ctrl, то можно выделить сразу несколько блоков текста (множественное выделение), когда начнем писать, текст будет вводиться одновременно во всех выделенных местах:


По нажатию Ctrl+P вызывается панель перехода и поиска. Если мы в ней введем @func, то перейдем к функции с именем func, если введем :10 - то перейдем к 10 строке.

Ctrl+Home, Ctrl+End - переход к началу или концу файла соответственно.

Ctrl + <-, Ctrl + -> - переход по словам

Сtrl + l - добавляем к выделенному еще одну строку

Сtrl + D  - 
выделяется текущее слово, а также подсвечиваются его другие occurences в документе. Последующие  нажатия этих клавиш будут выделять последующие  occurences этого слова. Удобно использовать при переименовании локальной переменной.

Ctrl + Shift + M - 
выделить текст внутри скобки, квадратной или фигурной. Повторяя нажатие этих клавиш, выделим и сами скобки, потом выделится текст внутри охватывающих эти скобки других скобок и т.д.:


Ctrl + F2 -поставить/убрать закладку. 
F2 -перейти к закладке

Sublime Package Control

Позволяет с легкостью устанавливать новые плагины. Устанавливается так. Нажимаем Ctrl + ` . Затем в открывшуюся консоль пишем


Перезапускаем Sublime. Идем в Preferences->Package Settings. Там должен появиться Package Control.
Плагин Alignment

Этот плагин помогает красиво выравнивать знаки равенства:


Для установки этого плагина нажимаем Ctrl + Shift + P. В строке набираем install и ищем Alignment.
Для того, чтобы сделать автоматическое выравнивание, набираем Ctrl + Alt + A. 

Плагин Emmet

Хорошо про этот плагин написано вот тут. Вкратце, этот плагин помогает сильно экономить время при создании макетов страниц. Например, можно написать 

ul>li*4>a[href=#]{текст}

нажать Tab, или Ctrl+E и получится вот что:


Плагин CSS Comb

Применение этого плагина позволяет сделать код CSS более читаемым и красивым, т.е. улучшает стиль кода. Используется он следующим образом. Встаем курсором на блок CSS и нажимаем Shift + Ctrl + C.

До и после:



Информационные источники: ПервыйВторойТретий и еще какие-то...

воскресенье, 15 сентября 2013 г.

Как пользоваться git: тонкости

Прошла часть туториалов, находящихся вот тут. Разъяснилось несколько вещей (см. ниже).

Во-первых, ветки (бранчи) в Git не занимают много места, т.к. по сути это просто ссылки на конкретный commit и больше ничего. Во-вторых, ...

Merge vs Rebase

Команда merge создает специальный commit, у которого есть два уникальных родителя. Этот commit как бы говорит нам "я хочу включить в себя всю ту работу, которая была проделана при разработке обоих моих родителей и их родителей". Рассмотрим следующий пример.

Введем команду 
git merge bugFix
Получится вот что:



Теперь в master'е содержится вся выполненная работа. Теперь выполним такие команды:

git checkout bugFix
git merge master


Т.к. ветка bugFix находилась в дереве выше (то есть, менее актуальна), чем master, то ссылка bugFix просто передвинулась на тот же commit, на который указывает ветка master.

Команда rebase берет несколько commit'ов, копирует их, и помещает копии вниз под тем commit'ом, который мы укажем. Преимущество rebase в том, что он приводит историю commit'ов к линейному виду. Commit log/history будет выглядеть гораздо более красивым, чем до применения rebase. Рассмотрим следующий пример.


Допустим, мы хотим перенести то, что сделано в bugFix, чтобы в истории commit'ов все выглядело так, как будто мы не параллельно разрабатывали две фичи, а последовательно. Введем команду 

git rebase master

Получится вот что:
Создался новый commit с фичей из ветки bugFix, являющийся продолжением commit'а C2. Теперь у нас красивая линейная история commit'ов. Заметим, что commit C3 до сих пор где-то болтается (где именно, не написано было), а commit C3' как бы является его копией.

Теперь введем команды 

git checkout master
git rebase bugFix

Получится вот что:
Т.к. ветка master находилась в дереве выше, чем bugFix, т.е. была менее актуальна, то она просто передвинулась.

Reset vs Revert

Reset отменяет изменения путем переноса ссылки на предыдущий commit. Это как бы переписывание истории commit'ов. Пусть у нас вот такая история commit'ов:


Наберем команду

git reset HEAD~1

(HEAD это относительная ссылка на commit, на котором мы сейчас находимся. Оператор ~ позволяет прыгать по дереву commit'ов вверх или вниз на указанное количество шагов, в данном случае на 1)

Получится вот что:


Эта команда отлично подходит для локальных репозиториев на вашем компьютере, но совершенно не подходит для случая удаленных репозиториев, когда над проектом работают несколько программистов (в этом случае до других просто не дойдут ваши изменения). В этих других случаях выручает команда revert.

Пусть у нас опять такая же ситуация, и мы хотим отменить последний commit:


Введем команду

git revert HEAD
И получится вот что:


Т.е. создался новый commit, аналогичный commit'у C1, не содержащий изменений, которые были в commit'е C2.

Cherry-pick

Эта команда позволяет более гибко перемещать commit'ы. Имеет следующий формат: cherry-pick <Commit i1><Commit i2>...<Commit ik>. Рассмотрим пример. Пусть у нас имеется вот такой репозиторий

 И мы хотим перенести в master все фичи, которые есть в commit'ах C3, C4, C7. Наберем команду

git cherry-pick C3 C4 C7

И получим желаемый результат:

И вот еще, про гигиену commit'ов:
  • Коммитить нужно часто, но атомарно (т.е. только законченные функции), также нужно подробно описывать, что в commit'е (иначе будет трудно поймать состояние проекта, будет трудно понять, куда нужно откатиться, если текущая версия не работает).
  • Не использовать git как систему бэкапа!

пятница, 13 сентября 2013 г.

Как называть переменные и функции: rules of thumb

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

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

Плохо:
int d: //Прошедшее время
Хорошо:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

Плохо:
Хорошо:

2. Избегайте дезинформации.

2.1. Переменным не стоит присваивать имена типа hp, aix и sco, потому что они ассоциируются с платформами и разновидностями Unix. Даже если в переменной хранится длина гипотенузы и имя hp кажется хорошим сокращением, оно может ввести в заблуждение читателя кода.

2.2. Не обозначайте группу учетных записей именем accountList,  если только она действительно не хранится в списке. Лучше подойдут такие имена, как accountGroup, bunchOfAccounts или даже просто accounts. 

2.3. Остерегайтесь малозаметных различий в именах. Сколько времени потребуется, чтобы заметить незначительное различие в XYZControllerForEfficientHandlingOfStrings и XYZControllerForEfficientStorageOfStrings?

2.4. Не используйте комбинации из "l" и "O" в именах переменных, потому что они похожи на 0 и 1.

Плохо:
int a = l;
if ( O == l )
    a = ol;
else
    l = ol;

3. Используйте осмысленные различия

Плохо:
Хорошо:
Вместо a1 и a2 лучше использовать source и destination.

Плохо:
-Классы Product, ProductInfo, ProductData  - разные имена, которые по сути обозначают одно и тоже. 
-Методы getActiveAccount(), getActiveAccounts(),  getActiveAccountInfo(). Как понять, какую из этих функций вызывать в конкретном случае?

4. Используйте удобопроизносимые имена.
Если имя невозможно нормально произнести, то при любом его упоминании в обсуждении вы выглядите полным идиотом. "Итак, за этим би-си-эр-три-си-эн-тэ у нас идет пи-эс-зэт-кью, видите?"

5. Однобуквенные имена могут использоваться только для локальных переменных в коротких методах. Длина имени должна соответствовать размеру его области видимости. Однобуквенные переменные хорошо использовать в счетчиках цикла.

6. Имена классов и объектов должны представлять собой существительные и их комбинации: Customer, WikiPage, Account, AddressParser. Старайтесь не использовать в именах классов такие слова, как Manager,  Processor, Data, Info. Имя класса не должно быть глаголом. 

7. Имена методов представляют собой глаголы или глагольные сочетания: postPayment, deletePage, save и т.д. Методы чтения/записи и предикаты образуются из значения и префикса get, set, is.
Хорошо:
string name = employee.getName();
customer.setName("mike");
if (paycheck.isPosted())

8. Выберите одно слово для каждой концепции. Например, существование в разных классах эквивалентных методов с именами fetch, retrieve и get неизбежно создаст путаницу. Аналогичным образом, использование терминов controller, manager и driver в одной кодовой базе тоже вызывает путаницу. Чем DeviceManager принципиально отличается от ProtocolController?

9. Старайтесь не использовать одно слово в двух смыслах. Допустим, программа содержит много классов с методами add, которые создают новое значение сложением или конкатенацией двух существующих значений. Вы пишете новый класс с методом, помещающим свой единственный параметр в коллекцию. Стоит ли присвоить этому методу имя add? Т.к. новый метод имеет другую семантику, то ему лучше присвоить имя insert или append.

10. Не стесняйтесь использовать термины из области информатики, названия алгоритмов и паттернов, математические термины и т.д. Имя AccountVisitor сообщит много полезной информации программисту, знакомому с паттерном "Посетитель". Если для того, что вы делаете, не существует подходящего программизма, используйте имя из пространства задачи. По крайней мере программист, занимающийся сопровождением кода, сможет узнать у специалиста в предметной области, что означает это имя.