Чистый код. Создание, анализ и рефакторинг | Роберт Мартин


Пересказ своими словами, коротко и по делу. В форме заметок.

Предисловие. Введение

В этих главах нас постепенно погружают в книгу, начинают раскрывать понятие "Чистый код". В основе всего в программировании, в 80% случаев, лежит сопровождение продуктов, их кодовой базы, а написание чистого кода поможет сделать это сопровождение более приятным процессом.

"Чистота ведёт к Божественности". Нас настраивают на то, что умение писать чистый код — тяжёлая работа, и что эта работа не ограничивается знанием паттернов и принципов. Книга поделена на три части.

  • В первой части излагаются принципы, паттерны и приёмы написания чистого кода.
  • Вторая часть состоит из практических сценариев с упражнениями по чистке кода.
  • В третей части подводятся итоги всего сказанного раннее.

Глава 1. Чистый код

В этой главе нам напоминают, о чём будет эта книга, и о том, что она полна реальными примерами кода. Автор говорит о том, что плохой код неизбежно ведёт компанию к краху, Мартин приводит пример реальной компании, которая гналась за тиражом своего продукта и упустила его медленное убийство из-за того, что код не рефакторился и постепенно усложнялся до такой степени, что с ним было невозможно работать.

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

  • Чистый код — это элегантность.
  • Чистый код должен быть простым и прямолинейным.
  • Чистый код может читаться и усовершенствоваться другими разработчиками, кроме его исходного автора (главное, чтобы другие разработчики не были говнокодерами).
  • Чистый код проходит все тесты, не содержит дубликаты, выражает все концепции проектирования, заложенные в систему, содержит минимальное количество сущностей.

Хорошо писать код недостаточно — необходимо поддерживать чистоту кода с течением времени. В конце главы, конечно же, не обошлось без небольшой саморекламы: автор напоминает нам, что у него есть книга "Agile Software Development: Principles, Patterns and Practices" (PPP), посвящённая принципам ООП и практическим приёмам.

Глава 2. Содержательные имена

Имена, имена, имена... То, что делается так часто, должно делаться хорошо. Далее приводятся некоторые простые правила создания хороших имён.

Основные принципы. Выжимка:

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

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

  • Используйте осмысленные различия. Когда программист пишет код исключительно для удовлетворения требований компилятора или интерпретатора, он сам себе создаёт проблемы. Неинформативные слова применяются для создания бессодержательных различий. Слово variable никогда не должно встречаться в именах переменных. Слово table никогда не должно встречаться в именах таблиц. Чем NameString лучше Name? Разве имя может быть, скажем, вещественным числом? Если может, то это нарушает предыдущее правило о дезинформации.

  • Используйте удобопроизносимые имена. Людям удобно работать со словами. Значительная часть нашего мозга специализируется на концепции слов, а слова по определению удобопроизносимы.

  • Выбирайте имена, удобные для поиска

  • Избегайте схем кодирования имён

  • Избегайте мысленных преобразований. Не заставляйте читателя мысленно преобразовывать ваши имена в другие, уже известные ему. Обычно эта проблема возникает из-за нежелания использовать понятия как из пространства задачи, так и из пространства решения.

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

  • Имена методов представляют собой глаголы или глагольные словосочетания: postPayment, deletePage, save и т. д. Методы чтения/записи и предикаты образуются из значения и префикса get, set и is.

  • Выберите одно слово для каждой концепции. Выберите одно слово для представления одной абстрактной концепции и придерживайтесь его. Например, существование в разных классах эквивалентных методов с именами fetch, retrieve и get неизбежно создаст путаницу.

  • Используйте имена из пространства решения. Не забывайте: ваш код будут читать программисты. А раз так, не стесняйтесь использовать термины из области информатики, названия алгоритмов и паттернов, математические термины и т. д. Имя AccountVisitor сообщит много полезной информации программисту, знакомому с паттерном «Посетитель» (Visitor). И какой программист не знает, что такое «очередь заданий» (JobQueue)?

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

Глава 3. Функции

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

Основные принципы. Выжимка:

  • Компактность. Первое правило: функции должны быть компактными. Второе правило: функции должны быть ещё компактнее. Строки не должны состоять из 150 символов, а функции — из 100 строк. Желательно, чтобы длина функции не превышала 20 строк.

  • Блоки и отступы. Блоки в командах if, else, while и т.д. должны состоять из одной строки, в которой обычно содержится вызов функции. Это не только делает вмещающую функцию более компактной, но и способствует документированию кода, поскольку вызываемой в блоке функции можно присвоить удобное содержательное имя. Кроме того, функции не должны содержать вложенных структур, так как это приводит к их увеличению. Максимальный уровень отступов в функции не должен превышать одного-двух. Это упрощает чтение и понимание функций.

  • Правило одной операции. Функция должна выполнять только одну операцию. Она должна выполнять её хорошо и ничего другого не должна делать. Если функция выполняет только те действия, которые находятся на одном уровне под объявленным именем функции, то эта функция выполняет одну операцию. Чтобы определить, выполняет ли функция более одной операции, попробуйте извлечь из неё другую функцию, которая бы не являлась простой переформулировкой реализации.

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

  • Чтение кода сверху вниз: правило понижения. Код должен читаться как рассказ — сверху вниз. За каждой функцией должны следовать функции следующего уровня абстракции. Это позволяет читать код, последовательно спускаясь по уровням абстракции в ходе чтения списка функций. Такой подход называется «правилом понижения».

  • Команды switch. Общее правило в отношении команд switch гласит, что эти команды допустимы, если они встречаются в программе однократно, используются для создания полиморфных объектов и скрываются за отношениями наследования, чтобы оставаться невидимыми для остальных частей системы.

  • Используйте содержательные имена. Не бойтесь использовать длинные имена. Длинное содержательное имя лучше короткого невразумительного. Выбор содержательных имён прояснит архитектуру модуля и поможет вам усовершенствовать её. Нередко поиски хороших имён приводят к полезной реструктуризации кода.

  • Аргументы функций. В идеале количество аргументов функции равно нулю (нуль-арная функция). Далее следуют функции с одним аргументом (унарные) и с двумя аргументами (бинарные). Функций с тремя аргументами (тернарных) следует по возможности избегать. Необходимость функций с большим количеством аргументов (полиарных) должна быть подкреплена очень вескими доводами — и всё равно такие функции лучше не использовать. Аргументы усложняют функции и лишают их значительной части концептуальной мощи. Аргументы создают ещё больше проблем с точки зрения тестирования. Представьте, как трудно составить все тестовые сценарии, проверяющие правильность работы кода со всеми комбинациями аргументов. Если аргументов нет, задача тривиальна.

  • Аргументы-флаги. Аргументы-флаги уродливы. Передача логического значения функции — поистине ужасная привычка. Она немедленно усложняет сигнатуру метода, громко провозглашая, что функция выполняет более одной операции. При истинном значении флага выполняется одна операция, а при ложном — другая.

  • Объекты как аргументы. Если функция должна получать более двух или трёх аргументов, весьма вероятно, что некоторые из этих аргументов стоит упаковать в отдельном классе.

  • Списки аргументов. Функции с переменным списком аргументов могут быть унарными, бинарными и даже тернарными, но использовать большее количество аргументов было бы ошибкой.

  • Глаголы и ключевые слова. Выбор хорошего имени для функции способен в значительной мере объяснить смысл функции, а также порядок и смысл её аргументов. В унарных функциях сама функция и её аргумент должны образовывать естественную пару «глагол/существительное».

  • Избавьтесь от побочных эффектов. Побочные эффекты — это ложь. Побочный эффект создаёт временную привязку.

  • Выходные аргументы. Выходных аргументов следует избегать. Если ваша функция должна изменять чьё-то состояние, пусть она изменяет состояние своего объекта-владельца.

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

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

  • Изолируйте блоки try/catch. Блоки try/catch выглядят весьма уродливо. Они запутывают структуру кода и смешивают обработку ошибок с нормальной обработкой.

  • Обработка ошибок как одна операция. Функции должны выполнять одну операцию. Обработка ошибок — это одна операция. Значит, функция, обрабатывающая ошибки, ничего другого делать не должна. Если в функции присутствует ключевое слово try, то оно должно быть первым словом в функции, а после блоков catch/finally ничего другого быть не должно.

  • Не повторяйтесь. Дублирование создаёт проблемы, потому что оно увеличивает объём кода. При изменении алгоритма вам придётся вносить изменения сразу в нескольких местах, что увеличивает вероятность ошибки.

Глава 4. Комментарии

Ничто не помогает так, как уместный комментарий. Ничто не загромождает модуль так, как бессодержательные и безапелляционные комментарии. Ничто не приносит столько вреда, как старый, утративший актуальность комментарий, распространяющий ложь и дезинформацию. Грамотное применение комментариев должно компенсировать нашу неудачу в выражении своих мыслей в коде. Обратите внимание на слово «неудачу». Комментарий — всегда признак неудачи. Мы вынуждены использовать комментарии, потому что нам не всегда удается выразить свои мысли без них, однако гордиться здесь нечем. Чем древнее комментарий, чем дальше он расположен от описываемого им кода, тем больше вероятность того, что он просто неверен. Причина проста: программисты не могут нормально сопровождать комментарии. Только код может правдиво сообщить, что он делает. Это единственный источник действительно достоверной информации.

Основные принципы. Выжимка:

  • Комментарии не компенсируют плохого кода. Одной из распространенных причин для написания комментариев является низкое качество кода. Однако комментарии не должны использоваться как оправдание для плохого кода.

  • Хорошие комментарии. Необходимые и полезные комментарии все же существуют. Следует помнить, что по-настоящему хороший комментарий — тот, без которого вам удастся обойтись.

  • Юридические комментарии. Иногда корпоративные стандарты кодирования заставляют нас вставлять комментарии по юридическим соображениям. Вместо того чтобы перечислять в комментарии все условия, по возможности ограничьтесь ссылкой на стандартную лицензию или другой внешний документ.

  • Информативные комментарии. Иногда бывает полезно включить в комментарий пояснение к коду.

  • Представление намерений. Иногда комментарий выходит за рамки полезной информации о реализации и описывает намерения, заложенные в решение.

  • Прояснение. Иногда смысл загадочного аргумента или возвращаемого значения бывает удобно преобразовать в удобочитаемую форму.

  • Предупреждения о последствиях. Иногда бывает полезно предупредить других программистов о нежелательных последствиях от каких-либо действий.

  • Комментарии TODO. Иногда бывает полезно оставить заметки «на будущее» в форме комментариев //TODO. Такие комментарии напоминают о том, что, по мнению программиста, сделать необходимо, но по какой-то причине нельзя сделать прямо сейчас. Однако код не должен загромождаться лишними комментариями TODO. Регулярно просматривайте их и удаляйте те, которые потеряли актуальность.

  • Усиление. Комментарий может подчеркивать важность обстоятельства, которое на первый взгляд кажется несущественным.

  • Плохие комментарии. Большинство комментариев относится именно к этой категории. Обычно такие комментарии представляют собой «подпорки» для некачественного кода или оправдания сомнительных решений, а их текст напоминает рассуждения вслух самого программиста.

  • Бормотание. Не стоит лепить комментарии «на скорую руку» только потому, что вам кажется, что это уместно или этого требует процесс. Если уж вы решаете написать комментарий, не жалейте времени и напишите лучший из всех возможных комментариев.

  • Избыточные комментарии. Избыточные комментарии не объясняют код, не предоставляют обоснований и не раскрывают намерений. Они читаются не проще, чем сам код. Более того, комментарий уступает коду в точности и навязывает читателю эту неточность взамен истинного понимания.

  • Недостоверные комментарии. Иногда с самыми лучшими намерениями программист делает в комментариях заявления, неточные и не соответствующие истине. Это может привести к ошибкам и недоразумениям.

  • Обязательные комментарии. Правила, требующие, чтобы каждая функция или переменная имела комментарий, — обычная глупость. Такие комментарии только загромождают код, распространяют недостоверную информацию и вызывают общую путаницу и дезориентацию.

  • Журнальные комментарии. Некоторые программисты добавляют комментарий в начало модуля при каждом его редактировании. Эти комментарии накапливаются, образуя своего рода журнал всех вносимых изменений. В наши дни длинные журналы только загромождают и усложняют код. Их следует полностью удалить из ваших программ.

  • Шум. Также в программах нередко встречаются комментарии, не содержащие ничего, кроме «шума». Они лишь утверждают очевидное, не предоставляя никакой новой информации.

  • Не используйте комментарии там, где можно использовать функцию или переменную.

  • Позиционные маркеры. Некоторые программисты любят отмечать определенные позиции в исходных файлах. В отдельных случаях объединение функций под такими заголовками имеет смысл. Но в общем случае они составляют балласт, от которого следует избавиться.

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

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

  • Закомментированный код. В программировании редко встречаются привычки более отвратительные, чем закомментированный неиспользуемый код. Никогда не делайте этого! У других программистов, видящих закомментированный код, не хватает храбрости удалить его. В итоге закомментированный код скапливается, словно осадок на дне бутылки плохого вина.

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

  • Слишком много информации. Не включайте в комментарии исторические дискуссии или описания подробностей, не относящиеся к делу.

  • Неочевидные комментарии. Связь между комментарием и кодом, который он описывает, должна быть очевидной.

  • Заголовки функций. Короткие функции не нуждаются в долгих описаниях. Хорошо выбранное имя компактной функции, которая выполняет одну операцию, обычно лучше заголовка с комментарием.

Глава 5. Форматирование

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

Основные принципы. Выжимка:

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

  • Вертикальное форматирование. Достаточно серьезную систему можно построить из файлов, типичная длина которых составляет 200 строк, с верхним пределом в 500 строк. Хотя это не должно считаться раз и навсегда установленным правилом, такие показатели весьма желательны. Маленькие файлы обычно более понятны, чем большие.

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

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

  • Вертикальное сжатие. Если вертикальные пропуски разделяют концепции, то вертикальное сжатие подчеркивает тесные связи. Таким образом, строки кода, между которыми существует тесная связь, должны быть «сжаты» по вертикали.

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

  • Объявления переменных. Переменные следует объявлять как можно ближе к месту использования. В коротких функциях локальные переменные должны перечисляться в начале каждой функции. Переменные экземпляров, напротив, должны объявляться в начале класса. Это не увеличивает вертикальное расстояние между переменными, потому что в хорошо спроектированном классе они используются многими, если не всеми, методами класса. Важно, чтобы переменные экземпляров объявлялись в одном хорошо известном месте, где все могут их найти.

  • Зависимые функции. Если одна функция вызывает другую, то эти функции должны располагаться вблизи друг от друга по вертикали, а вызывающая функция должна находиться над вызываемой (если это возможно). Это формирует естественную структуру программного кода.

  • Концептуальное родство. Некоторые фрагменты кода требуют размещения вблизи от других фрагментов. Такие фрагменты обладают определенным концептуальным родством. Чем сильнее родство, тем меньше должно быть вертикальное расстояние между ними. Родство может быть основано на прямой зависимости (когда одна функция вызывает другую) или на использовании переменных в функциях. Однако существуют и другие разновидности родства. Например, родство возникает в том случае, если группа функций выполняет аналогичные операции.

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

  • Горизонтальное форматирование. Хорошо установить себе «верхнюю планку» в 120 символов.

  • Горизонтальное разделение и сжатие. Горизонтальные пропуски используются для группировки взаимосвязанных элементов и разделения разнородных элементов.

  • Горизонтальное выравнивание. Предпочтение следует отдавать невыровненным объявлениям и присваиваниям, потому что они помогают выявить один важный дефект. Если в программе встречаются длинные списки, нуждающиеся в выравнивании, то проблема кроется в длине списка, а не в отсутствии выравнивания.

  • Правила форматирования в группах. У каждого программиста есть свои любимые правила форматирования, но если он работает в группе, то должен руководствоваться групповыми правилами. Группа разработчиков согласует единый стиль форматирования, который в дальнейшем применяется всеми участниками. Код программного продукта должен быть оформлен в едином стиле. Он не должен выглядеть так, словно был написан несколькими личностями, расходящимися во мнениях по поводу оформления.

## 18-07-2024

designed by cbrwvy