«Чистый код» Роберт Мартин. Конспект. Как писать понятный и красивый код?

от автора

Я решил написать конспект книги, которая всем известна, а сам автор называет ее «Школой учителей Чистого кода».
Пристальный взгляд Мартина как бы говорит:
«Я тебя насквозь вижу. Ты опять не следуешь принципам чистого кода?»
image

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

Что же такое этот самый чистый код по версии Мартина в нескольких словах? Это код без дублирования, с минимальным количеством сущностей, удобный для чтения, простой. В качестве девиза можно было бы выбрать: «Ясность превыше всего!».

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

Имена должны передавать намерения программиста

Имя переменной, функции или класса должно сообщить, почему эта переменная существует, что она делает и как используется. Если имя требует дополнительных комментариев, значит, оно не передает намерений программиста. Лучше написать, что именно измеряется и в каких именно единицах.
Пример хорошего названия переменной: daysSinceCreation;
Цель: убрать неочевидность.

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

Не используйте слова со скрытыми значениями, отличными от предполагаемого. Остерегайтесь малозаметных различий в именах. Например, XYZControllerForEfficientHandlingOfStrings и XYZControllerForEfficientStorageOfStrings.
По-настоящему устрашающие примеры дезинформирующих имен встречаются при использовании строчной «L» и прописной «O» в именах переменных, особенно в комбинациях. Естественно, проблемы возникают из-за того, что эти буквы почти не отличаются от констант «1» и «0» соответственно.

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

Если имена различаются, то они должны обозначать разные понятия.
«Числовые ряды» вида (a1, a2,… aN) являются противоположностью сознательного присваивания имен. Они не несут информации и не дают представления о намерениях автора.
Не информативные слова избыточны. Слово variable никогда не должно встречаться в именах переменных. Слово table никогда не должно встречаться в именах таблиц. Чем имя NameString лучше Name? Разве имя может быть, скажем, вещественным числом?
Используйте удобопроизносимые имена: generationTimestamp намного лучше genymdhms.

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

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

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

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

Интерфейсы и реализации

Я (автор книги) предпочитаю оставлять имена интерфейсов без префиксов. Префикс I, столь распространенный в старом коде, в лучшем случае отвлекает, а в худшем — передает лишнюю информацию. Я не собираюсь сообщать своим пользователям, что они имеют дело с интерфейсом.

Имена классов

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

Имена методов

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

Воздержитесь от каламбуров

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

Добавьте содержательный контекст

Контекст можно добавить при помощи префиксов: addrFirstName, addrLastName, addrState и т. д. По крайней мере читатель кода поймет, что переменные являются частью более крупной структуры. Конечно, правильнее было бы создать класс с именем Address, чтобы даже компилятор знал, что переменные являются частью чего-то большего.

Переменные с неясным контекстом

private void printGuessStatistics(char candidate, int count) {     String number;     String verb;     String pluralModifier;     if (count == 0) {       number = "no";       verb = "are";       pluralModifier = "s";     } else if (count == 1) {       number = ^_^quotquot^_^;       verb = "is";       pluralModifier = "";     } else {       number = Integer.toString(count);       verb = "are";       pluralModifier = "s";     }     String guessMessage = String.format(       "There %s %s %s%s", verb, number, candidate, pluralModifier     );     print(guessMessage);   } 

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

Переменные с контекстом

public class GuessStatisticsMessage {   private String number;   private String verb;   private String pluralModifier;   public String make(char candidate, int count) {     createPluralDependentMessageParts(count);     return String.format(       "There %s %s %s%s",         verb, number, candidate, pluralModifier );   }   private void createPluralDependentMessageParts(int count) {     if (count == 0) {       thereAreNoLetters();     } else if (count == 1) {       thereIsOneLetter();     } else {       thereAreManyLetters(count);     }   }   private void thereAreManyLetters(int count) {     number = Integer.toString(count);     verb = "are";     pluralModifier = "s";   }   private void thereIsOneLetter() {     number = ^_^quotquot^_^;     verb = "is";     pluralModifier = "";   }   private void thereAreNoLetters() {     number = "no";     verb = "are";     pluralModifier = "s";   } }

Не добавляйте избыточный контекст

Короткие имена обычно лучше длинных, если только их смысл понятен читателю кода. Не включайте в имя больше контекста, чем необходимо.

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

Компактность!

Первое правило: функции должны быть компактными.
Второе правило: функции должны быть еще компактнее.
Мой практический опыт научил меня (ценой многих проб и ошибок), что функции должны быть очень маленькими. Желательно, чтобы длина функции не превышала 20 строк.

Правило одной операции

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

Секции в функциях

Функцию, выполняющую только одну операцию, невозможно осмысленно разделить на секции.

Один уровень абстракции на функцию

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

Чтение кода сверху вниз: правило понижения

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

Команды switch

Написать компактную команду switch довольно сложно. Даже команда switch
всего с двумя условиями занимает больше места, чем в моем представлении
должен занимать один блок или функция. Также трудно создать команду switch,
которая делает что-то одно — по своей природе команды switch всегда выполняют
N операций. К сожалению, обойтись без команд switch удается не всегда, но по
крайней мере мы можем позаботиться о том, чтобы эти команды были скрыты
в низкоуровневом классе и не дублировались в коде. И конечно, в этом нам может
помочь полиморфизм.

В примере представлена всего одна операция, зависящая от типа работника.

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);     }   }

Эта функция имеет ряд недостатков. Во-первых, она велика, а при добавлении
новых типов работников она будет разрастаться. Во-вторых, она совершенно
очевидно выполняет более одной операции. В-третьих, она нарушает принцип
единой ответственности (Single responsibility principle), так как у нее существует несколько возможных причин изменения.

В-четвертых, она нарушает принцип открытости/закрытости (The Open Closed Principle), потому что код функции должен изменяться при каждом добавлении новых типов.

Но, пожалуй, самый серьезный недостаток заключается в том, что программа
может содержать неограниченное количество других функций с аналогичной
структурой, например:
isPayday(Employee e, Date date)
или
deliverPay(Employee e, Money pay)
и так далее.

Все эти функции будут иметь все ту же ущербную структуру. Решение проблемы заключается в том, чтобы похоронить команду switch в фундаменте абстрактной фабрики и никому ее не показывать. Фабрика использует команду switch для создания соответствующих экземпляров потомков Employee, а вызовы функций calculatePay, isPayDay, deliverPay и т. д. проходят полиморфную передачу через интерфейс Employee.

public abstract class Employee {   public abstract boolean isPayday();   public abstract Money calculatePay();   public abstract void deliverPay(Money pay); } ----------------- public interface EmployeeFactory {   public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; } ----------------- public class EmployeeFactoryImpl implements EmployeeFactory {   public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {     switch (r.type) {       case COMMISSIONED:         return new CommissionedEmployee(r) ;       case HOURLY:         return new HourlyEmployee(r);       case SALARIED:         return new SalariedEmploye(r);       default:         throw new InvalidEmployeeType(r.type);     }   } }

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

Используйте содержательные имена

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

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

Аргументы функций

В идеальном случае количество аргументов функции равно нулю. Далее следуют функции с одним аргументом (унарные) и с двумя аргументами (бинарные). Функций с тремя аргументами (тернарных) следует по возможности избегать.

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

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

Аргументы-флаги

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

Бинарные функции

Функцию с двумя аргументами понять сложнее, чем унарную функцию.
Конечно, в некоторых ситуациях форма с двумя аргументами оказывается
уместной. Например, вызов Point p = new Point(0,0); абсолютно разумен. Однако
два аргумента в нашем случае являются упорядоченными компонентами одного
значения.

Объекты как аргументы

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

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

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

Глаголы и ключевые слова

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

Читатель понимает, что чем бы ни было «имя» (name), оно куда-то «записывается» (write). Еще лучше запись writeField(name), которая сообщает, что «имя» записывается в «поле» какой-то структуры.

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

Разделение команд и запросов

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

Изолируйте блоки try/catch

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

Обработка ошибок как одна операция

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

На этом глава 3 заканчивается.

ссылка на оригинал статьи https://habr.com/ru/post/485118/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *