Книга «Пять строк кода. Роберт Мартин рекомендует»

imageХаброжители, как дела?
А у нас тут книга по рекомендации самого Дяди Боба.

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

Познакомьтесь с уникальным подходом, позволяющим реализовать любой метод в пяти строках кода. И не забывайте про тайну, хорошо известную большинству senior-разработчиков: иногда проще ухудшить код и вернуться к его исправлению позже.

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

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

Пусть код типа работает


В конце предыдущей главы мы просто ввели функцию handleInput, для которой не могли использовать «Извлечение метода», потому что не хотели разрывать цепочку else if. К сожалению, handleInput не вписывается в наше основополагающее правило «Пять строк», поэтому оставлять все как есть нельзя.

Вот эта функция.

Листинг 4.1. Начальная форма

function handleInput(input: Input) {
     if (input === Input.LEFT) moveHorizontal(-1);
     else if (input === Input.RIGHT) moveHorizontal(1);
     else if (input === Input.UP) moveVertical(-1);
     else if (input === Input.DOWN) moveVertical(1);
}


4.1. РЕФАКТОРИНГ ПРОСТОЙ ИНСТРУКЦИИ IF


Здесь мы немного застряли. Чтобы показать вам, как обрабатывать подобные цепочки else if, я начну с введения нового правила.

4.1.1. Правило «Никогда не использовать if с else»


Утверждение

Никогда не используйте if с else, если только не выполняете проверку в отношении типа данных, который не контролируете.

Объяснение

Принимать решения бывает непросто. В реальной жизни многие люди склонны этого избегать и постоянно откладывают решение на потом. А вот в коде мы используем инструкции if-else активно. Я не стану утверждать, как лучше действовать в жизни, но в коде ожидание определенно является более удачной тактикой. Если мы используем if-else, то фиксируем точку, в которой программа принимает решение. Это снижает гибкость кода, поскольку исключает возможность внесения вариативности после блока if-else.

Конструкции if-else можно рассматривать как жестко закодированные решения. Однако подобно тому, как нам не нравятся жестко прописанные в коде константы, так же не нравятся и жестко прописанные решения.

Лучше никогда не прописывать решение жестко, то есть никогда не использовать if с else. К сожалению, при этом необходимо обращать внимание на то, относительно чего выполняется проверка. Например, с помощью e.key мы проверяем, какая клавиша нажата, здесь у нас используется тип string. Реализацию string мы изменить не можем, значит, не можем избежать и цепочки else if.

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

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

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

window.addEventListener("keydown", e => {
     if (e.key === LEFT_KEY || e.key === "a") inputs.push(Input.LEFT);
     else if (e.key === UP_KEY || e.key === "w") inputs.push(Input.UP);
     else if (e.key === RIGHT_KEY || e.key === "d") inputs.push(Input.RIGHT);
     else if (e.key === DOWN_KEY || e.key === "s") inputs.push(Input.DOWN);
});


Мы не имеем контроля над любым из двух типов данных в этих условиях: KeyboardEvent и string. Как и говорилось, эти цепочки else if должны быть напрямую связаны с вводом/выводом, который, в свою очередь, должен быть отделен от остальной части приложения.

Обратите внимание, что мы считаем отдельные if проверками, а if-else — решениями. Это позволяет проводить простую проверку в начале методов, где было бы сложно извлечь ранний возврат return, как в следующем примере. То есть это правило конкретно нацелено на else.

Помимо этого, оно легко проверяется: достаточно просто найти else. Вернемся к более ранней функции, которая получает массив чисел и находит их среднее. Если вызвать предыдущую реализацию с пустым массивом, то мы получим ошибку деления на нуль. В этом есть смысл, потому что мы эту реализацию знаем, но для пользователя такая ошибка окажется бесполезной. Значит, желательно более широко идентифицировать (выбросить) ошибку через throw. Вот два способа исправить это.


Запах

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

Раннее связывание не позволяет делать изменение путем добавления, потому что мы можем изменить инструкцию if, только модифицировав ее с последующей компиляцией. В свою очередь, позднее связывание позволяет использовать простое добавление, что намного предпочтительнее. Об этом мы говорили в главе 2.

Намерение

Инструкции if выступают в качестве операторов потока управления. Это означает, что они определяют, какой код должен выполняться далее. Но в объектно-ориентированном программировании есть намного более сильные операторы потока управления: объекты. Если использовать интерфейс с двумя реализациями, то мы сможем при выполнении решить, какой код выполнять, в зависимости от инстанцируемого класса. По сути, это правило вынуждает нас искать способы использовать объекты, которые являются более эффективными и гибкими инструментами управления.

4.1.2. Применение правила


Первым шагом для избавления от if-else в handleInput будет замена перечисления Input интерфейсом Input. После этого значения заменяются классами. В завершение — это самая восхитительная часть — из-за того, что теперь значения являются объектами, становится возможно переместить код внутри if в методы каждого из классов. Но для этого нам предстоит преодолеть несколько разделов книги, так что наберитесь терпения. Мы будем идти к заветной цели шаг за шагом.

1. Введем новый интерфейс с временным именем Input2, содержащий методы для четырех значений в нашем перечислении.

Листинг 4.5. Новый интерфейс

enum Input {
     RIGHT, LEFT, UP, DOWN
}
interface Input2 {
     isRight(): boolean;
     isLeft(): boolean;
     isUp(): boolean;
     isDown(): boolean;
}


2. Создадим четыре класса, соответствующие этим четырем значениям перечисления. Все методы, за исключением соответствующего конкретному классу, должны возвращать false. Заметьте: эти методы временные, в чем мы позже убедимся.

image


3. Переименуем перечисление в RawInput, после этого компилятор будет выдавать ошибку во всех местах, где используется это перечисление.
4. Изменим типы с Input на Input2 и заменим проверки равенства новыми методами.

image


5. Исправим последние ошибки внесением изменений.
6. В завершение везде переименуем Input2 в Input. На этом этапе код будет выглядеть так.
В шаблоне «Замена кода типа классами» включаем процесс создания перечислений в классы.

4.1.3. Шаблон рефакторинга «Замена кода типа классами»


Описание

Этот шаблон рефакторинга преобразует перечисление в интерфейс, при этом значения перечисления становятся классами. Подобное действие позволяет нам добавлять каждому значению свойства и локализовать функциональность, относящуюся к данному конкретному значению. Совместно с другим шаблоном рефакторинга, который рассмотрим следующим («Перемещение кода в классы», 4.1.5), это дает возможность вносить изменения путем добавления. Дело в том, что зачастую используются перечисления посредством switch или цепочек else if, разбросанных по всему приложению. Инструкция switch определяет, как каждое возможное значение перечисления должно обрабатываться в данном месте.

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

Обратите внимание, что код типа также оформляется иначе, чем перечисления. Любой целочисленный тип или любой тип, поддерживающий проверку тождественности ===, может выступать как код типа. Чаще всего используются int и enum. Вот пример подобного кода типа для размеров футболок.

Листинг 4.15. Начальный

const SMALL = 33;
const MEDIUM = 37;
const LARGE = 42;


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

1. Вводим новый интерфейс с временным именем. Этот интерфейс должен содержать методы для каждого из значений перечисления.
2. Создаем классы, соответствующие каждому значению перечисления. Все методы из этого интерфейса, кроме одного, соответствующего классу, должны делать return false.
3. Переименовываем перечисление. В результате компилятор сообщает об ошибке везде, где оно используется.
4. Изменяем старое имя типа на временное и заменяем проверки тождественности новыми методами.
5. Заменяем оставшиеся ссылки значениями перечислений инстанцированием новых классов.
6. Когда ошибок больше нет, везде переименовываем интерфейс, заменяя его имя постоянным.

Пример

Рассмотрим небольшой пример с перечислением сигналов светофора и функцией для определения момента, когда можно начинать движение.

Листинг 4.18. Начальный

enum TrafficLight {
     RED, YELLOW, GREEN
}
const CYCLE = [TrafficLight.RED, TrafficLight.GREEN, TrafficLight.YELLOW];
function updateCarForLight(current: TrafficLight) {
     if (current === TrafficLight.RED)
          car.stop();
     else
          car.drive();
}


Следуя описанному процессу, мы делаем так.
1. Вводим новый интерфейс с временным именем. Этот интерфейс должен содержать методы для каждого значения перечисления.

Листинг 4.19. Новый интерфейс

interface TrafficLight2 {
     isRed(): boolean;
     isYellow(): boolean;
     isGreen(): boolean;
}


2. Создаем классы, соответствующие каждому значению перечисления. Все методы интерфейса, кроме одного, соответствующего классу, должны осуществлять return false.

Листинг 4.20. Новые классы

class Red implements TrafficLight2 {
     isRed() { return true; }
     isYellow() { return false; }
     isGreen() { return false; }
}
class Yellow implements TrafficLight2 {
     isRed() { return false; }
     isYellow() { return true; }
     isGreen() { return false; }
}
class Green implements TrafficLight2 {
     isRed() { return false; }
     isYellow() { return false; }
     isGreen() { return true; }
}


3. Переименовываем перечисление. В результате компилятор сообщает об ошибках во всех местах использования этого перечисления.
4. Изменяем имя типов со старого на временное и заменяем проверки тождественности новыми методами.
5. Вместо оставшихся ссылок на значения перечисления используем инстанцированные новые классы.
6. В завершение, когда ошибок уже нет, везде даем интерфейсу постоянное имя.
Этот шаблон рефакторинга сам по себе не вносит кардинальных улучшений в код, но создает основу для существенных улучшений в дальнейшем. Наличие методов is для всех значений тоже считается запахом, так что пока мы заменили один запах другим. Но эти методы можно затем обработать по одному, тогда как значения перечисления были тесно связаны между собой и обрабатывать их по отдельности было невозможно. Важно отметить, что большинство методов is являются временными и существуют недолго — в примере мы избавимся от некоторых из них в текущей главе и от многих других в главе 5.

Об авторе
image


Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Мартин

© Habrahabr.ru