23 January

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

ProgrammingJavaPerfect codeООP
Я решил написать конспект книги, которая всем известна, а сам автор называет ее «Школой учителей Чистого кода». Пристальный взгляд Мартина как бы говорит:

«Я тебя насквозь вижу. Ты опять не следуешь принципам чистого кода?»

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 заканчивается.
Tags:книгикниги по программированиюсоветы разработчикусоветы программистамчистый кодроберт мартин
Hubs: Programming Java Perfect code ООP
+17
13.1k 221
Comments 15
Popular right now