Глава 1. Что такое чистый код?
Глава 2. Содержательные имена.
Глава 6. Объекты и структуры данных.
Код никогда не исчезет, потому что код представляет подробности тербований!
По мере накопления хфоса в коде производительность группы начинает снижаться, асимпотически приближаясь к нулю.
В том что код загнивает выноваты не менеджеры, начальство, дедлайны, клиенты или исходная архитектура. Виноваты мы и наш непрофессионализм.
Уметь отличать чистый код от грязного != уметь писать чистый код.
Что такое "чистый код"?:
- логика прямолинейна, каждая функция делает то, что вы ожидали
- зависимости минимальны и явно определены, чтобы упростить сопровождение
- читается, как хорошо написанная проза
- используются содержательные имена
- покрыт тестами и проходит все эти тесты
- не содержит дубликатов
- содержит минимальное кол-во сущностей: классов, методов, функций и т.д.
При написании кода пользуйтесь правилом бойскаутов - "оставляй место стоянки чище, чем оно было до твоего прихода". (Чистка не обязательно должна быть глобальной, можно присвоить более понятное имя переменной или разбить слишком большую функцию и т.д.)
Ничто не обладает абсолютной истиной, в том числе все советы описанные в этой книге
Имя переменной, ф-ии или класса должно отвечать на вопросы: почему эта переменная (и т.д.) существует, что она делает и как используется.
public List<int[]> getItem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
Какие данные храняться в theList? Чем так важен элемент с нулевым индексом? Что такое 4? Как будет использоваться возвращаемый список? Чтобы отвитить, не хватает контекста. Допустим, это игра "Сапер". Стоит написать более осмысленные имена переменных, избавится от магических чисел и описать массив int. В результате получим:
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell[0] == 4)
flaggedCells.add(cell);
return flaggedCells;
}
- Не обозначайте группу учетных записей
accountList
, если только она действительно не храниться в списке (лучше подойдутaccountGroup
,bunchOfAccounts
или простоaccounts
). - Остерегайтесь малозаметных различий в именах.
Допустим, есть класс Product
. Создав другой класс ProductInfo
или ProductData
, вы создаете разные имена, которые по сути обозначают одно и тоже. Нужно записывать различающиеся имена так, чтобы было понятно, какой смысл заложен в этих различиях.
Слово variable не должно встречаться в именах переменных, а table не должно встречаться в именах таблиц.
Имена должны нормально произноситься!
Неудачный выбор:
private Date genymdhms;
private Date modymdhms;
Так лучше:
private Date generationTimestamp;
private Date modificationTimestamp;
Однобуквенные имена могут использоваться ТОЛЬКО для локальных переменных в коротких методах. Длинна имени должна соответствовать размеру его области видимости. Если переменная или константа может использоваться в нескольких местах кодового блока, важно присвоить ей имя удобное для поиска (спойлер: в ней будет больше одной или пары букв).
Это запись при которой в первая буква в имени переменной означала код типа. Так было в досторические имена, сейчас все это не нужно, потому что компиляторы, редакторы и языки стали более продвинутыми.
Например, вы строите абстрактную фабрику для создания геометрических фигур. Фабрика это интерфейс, который реализуется конкретным классом. Как их назвать? IShapeFactory
и ShapeFactory
? Автор считает, что лучше без префикса, потому что это устарело и передает лишнюю информацию. Пользователям не нужно знать, что они имеют дело с интерфейсом, им достаточно знать, что это ShapeFactory
. И если есть необходимось закодировать в имени либо интерфейс либо реализацию, то лучше выбрать реализацию.
Имена классов и объектов - это существительные и их комбинации: Customer
, WikiPage
, Account
. Лучше не использовать такие слова, как Manager
, Processor
, Data
или Info
. Имя класса не должно быть глаголом.
Имена методов - глаголы и глагольные словосочетания: postPayment
, deletePage
, save
. К методам чтения/записи добавляется префикс set
, get
, is
.
При перезагрузке конструкторов используются статические методы-фабрики с именами, описывающими аргументы. Например, запись Complex fulcrumPoint = Complex.FormRealNumber(23.0);
обычно лучше записи Complex fulcrumPoint = Complex(23.0);
.
Никаких тут LOL
в именах переменных. А abort()
лучше, чем eatMyShorts()
:).
Например, существование в разных классах эквивалентных методов get
, retrieve
, get
неизбежно создаст путаницу. Как запомнить к какому классу относится то или иное имя метода? Аналогично, использование терминов controller
, manager
, driver
в одной кодовой базе вызывает путаницу. Например, чем DeviceManager
отличается от ProtocolController
? Почему не использовать одинаковые термины? Такие имена создают впечатление, что два объекта обладают разными типами и относятся к разным классам.
Старайтесь не использовать одно слово в двух смыслах. Например, прграмма содержит много классов с методами add
, которые складывают два значения. Вы пишете новый класс с медотом, помещающим один элемент в коллекцию. Стоит ли дать этому методу имя add
? На первый взгляд это последовательно, но метод содержит другую семантику, поэтому лучше присвоить имя insert
или append
.
Т.к. код будут читать программисты, то можно использовать термины из области информатики, названия алгоритмов, паттернов, мат. термины. Например JobQueue
.
Немногие имена содержательны сами по себе. Все остальные имена стоит помещать в определенный контекст, заключая их в классы, функции, пространства имен. В крайнем случае, контекст можно уточнить при помощи префикса.
Короткие имена лучше длинных, если их смысл понятен читателю. Имена accountAddress
и customerAddress
подходят для экземпляров класса Address
, но для класса такой выбор неудачен. Address
- хорошее имя для класса.
Первое правило: фун-ии должны быть компактными. Второе правило: фун-ии должны быть ещё компактнее. Желательно чтобы длина функции не превышала 20 строк.
Блоки в командах if, else, while
должны состоять из одной строки, в которой обычно содержится вызов фун-ии. Это не только делает вмещающуюю фун-ии более компактной, но и способствует документированию кода.
Функция должна выполнять только одну операцию. Она должна выполнять её хорошо и ничего другого она делать не должна. Если ф-ия выполняет только те действия, которые находятся на одном уровне под объявленным именем ф-ии, то эта ф-ия выполняет одну операцию.
Чем меньше и специализированнее ф-ия, тем проще выбрать для нее содержательное имя. Длинное содержательное имя, лучше короткого невразумительного.
Блоки try/catch выглядят уродливо, запутывают структуру кода и смешивают обработку ошибок с нормальной обработкой, поэтому лучше выносить эти блоки в отдельные ф-ии, например:
try {
deletePageAndReferences(page);
} catch (e) {
logError(e) {
}
Грамотное применние комментариев должно компенсировать нашу неудачу в выражении своих мыслей в коде. Комментарий - всегда признак неудачи. Чем древнее комментарий, чем дальше он расположен от описываемого им кода, тем больше вероятность, что он неверен.
Примеры хороших комментариев:
- Юридические: копирайт, лицензия.
- Информативные: пояснения для регулярок, поиска, формата даты/времени.
- Представление намерений: нетривиальные решения требующие пояснения.
- Прояснение: комментарии для возвращающих значений, являющихся частью библиотеки.
- Предупреждения о последствиях: предупреждения о нежелательных или опасных действиях.
- Комментарии TODO.
- Услиление: если нужно подчеркнуть важность чего-то, что на первый взгляд кажется несущественным.
Всё остальное плохие комметарии, но если вы их всё таки решили написать, постарайтесь чтобы они были, краткими, содержатльными, достоверными. Позиционные маркеры, ссылки на авторов, закомментированный код, HTML-комментарии, неочевидные комментарии - плохо. Но не стоит использовать комментарий там, где можно использовать. фу-ю или переменную.
Исходный файл должен выглядеть как газетная статья. Имя файла должно быть простым, но содержательным. Начальные блоки описывают высокоуровневые концепции и алгоритмы. Степень детализации увеличивается к концу файла.
Каждая строка это выражение или условие, а каждая группа строк представляет собой законченную мысль. Каждая такая мысль должна отделятся пустой строкой. Концепции тесно связанные друг с другом должны находится поблизости друг от друга по вертикали и не должны находится в разных файлах. Переменные нужно объявлять как можно ближе к месту использования, а переменные экземпляров класса, напротив, должны объявлятся в начале класса. Если одна фу-я вызывает другую, то они должны находится вблизи друг друга и вызывающая фу-я должна находится над вызываемой (по возможности).
Оптимальная длинна строки 100-120 символов. Горизонтальное выравнивание, в большенстве случаев, приносит неудобства, потому что выделяет совсем не то, что требуется и отвлекает читателя.
Если в некоторой системе нас интересует гибкость в добавлении новых типов данных, то в этой части системы предпочтение отдается объектной реализации. Если нам нужна гибкость расширения поведения, то лучше выбирать процедуры.
При обнаружении ошибок лучше инициировать исключение. Код вызова становится более понятным, а его логика не скрывается за кодом обработки ошибок. Используйте try-catch-finaly.
Попробуйте писать тесты, принудительно инициирующие исключения, а затем включите в обработчик поведение, обеспечивающее прохождение тестов.
Создавайте содержательные сообщения об ошибках и передавайте их своим исключениям. Каждое исключение должно содержать достаточно информации для определения источника ошибки.
Не возвращайте null при обработке ошибок. Никогда! Это создает дополнительную работу и есть вероятность, что ваше приложение однажды "уйдет в штопор". Вместо это верните исключение или если ваш код вызывает стороннее api, способный вернуть null, создайте для него обертку в виде метода, который инициирует исключение или возвращает объект особого случая (см. паттерн особый случай).
Возвращать null из методов плохо, но передавать null при вызове ещё хуже. Можно создать новый тип сключения и инициировать его в методе.
Если в продукте есть код вне вашего контроля, примите особые меры по его защите, чтобы будущие изменения не обходились слишком дорого. Можно воспользоваться обертками или использовать паттерн "Адаптер".
- Не пишите код пока не напишите отказной модульный тест
- Не пишите тест в объеме большем, чем необходимо для отказа. Невозможность компиляции тоже отказ.
- Не пишите код в большем объеме, чем нужно для прохождения текущего отказного теста
Тесты и код пишутся вместе, а тесты немного опережают код.
Тесты "на скорую руку" равносильны отсутствию тестов или даже хуже отсутствия.
Тесты должны менятся по мере развития продукта.
Хороший тест - удобочитаемый тест.
В каждой тестовой фу-ии должна тестироваться только одна концепция.
Чистые тесты должны обладать 5-ю характеристиками:
Быстрота (Fast) Тесты должны выполнятся быстро.
Независимость (Independent) Тесты не должны зависеть друг от друга. Один тест не должен создавать условия для выполнения следующего теста.
Повторяемость (Repeatable) Тесты должны довать повторяемые результаты в любой среде.
Очевидность (Self-Validating) Результат выполнения теста должен быть логический. Тест либо пройден, либо нет. Чтобы узнать результат, пользователь не должен лезть в логи.
Своевременность (Timely) Тесты должны писать своевременно, непосредственно перед написанием кода продукта.