1. Возвращаясь к простоте
Идея объектно-ориентированного дизайна заключается в том, что увеличение сложности по одному направлению приведет к снижению сложности по другому направлению. Рост сложности с точки зрения “понимаемости” системы (необходимые для понимания когнитивные усилия) компенсируется снижением сложности сопровождения и расширения системы.
Любой код должен удовлетворять двум зачастую противоречащим требованиям:
- быть достаточно конкретным, чтобы его можно было легко понять;
- быть достаточно абстрактным, чтобы его можно было легко изменить / расширить.
Конфликт краткосрочной выгоды и долгосрочных целей - актуально для кода.
Сопротивление абстракциям в коде
Именование методов - сложная задача, потому что необходимо:
- Идентифицировать выделяемый код
- Определить концепцию (абстракцию), которую этот код описывает
- Подобрать подходящее имя для этой концепции, ни в коем случае не привязываясь к реализации метода
По мере своего развития программисты решают все более и более сложные проблемы и постепенно свыкаются со сложностью кода. Эта привычка иногда приводит к вере в то, что сложность неизбежна, что она является неотъемлемой и естественной чертой любой законченной программы. Тем не менее, за этой сложностью есть следующая ступень развития - более высокий уровень простоты и порядка.
Бесконечно опытные программисты пишут не бесконечно сложный, а предельно простой код.
См. цитаты о простоте и сложности: Цитаты о простоте и сложности
Shameless Green означает максимально понятный, а не максимально гибкий код. В процессе написания такого кода приходится смиренно терпеть дублирование, если это помогает выявить основополагающие абстракции. Последующие тесты или новые требования предоставят точную информацию о том, как именно нужно улучшить код.
Понимаемость кода ценится выше его расширяемости. Именно этот подход приводит к хорошей архитектуре с помощью понятных тестов и “сопротивления абстракциям” (см. выше). Это не отменяет принцип DRY, в частности, но полагает, что дешевле потерпеть дублирование кода, чем оправиться от вреда, нанесенного неверными абстракциями.
2. “Бесстыдно зеленый” (Shameless Green) код, управляемый тестами
Quick green excuses all sins. Kent Beck.
Суть в том, что надо быстрее добраться до работающего решения. В этом процессе каждый тест отражает какую-то гипотезу, и отсеивание плохих идей - это движение вперёд.
Принцип DRY важен, но может навредить, если его применять слишком рано и слишком фанатично
Вопросы, которые нужно задавать при обнаружении дублирования кода:
- Изменение, которое я рассматриваю, сделает код более сложным для понимания? Верные абстракции облегчают понимание кода, все остальные - неверны.
- Какова будущая цена нынешнего бездействия? Иногда изменения стоят одинаково сейчас и в будущем, поэтому их можно отложить.
- Когда я получу больше информации? При ТДД каждый следующий тест дает больше информации, поэтому можно потерпеть дублирование на протяжении нескольких тестов, надеясь чуть позже получить всю необходимую для принятия правильных решений информацию.
Разница между if/else
и switch
if/else
означает, что каждое последующее условие отличается каким-то значимым
образом. По идее, каждый новый оператор else if
проверяет условие с совершенно
новой смысловой нагрузкой, поэтому при чтении кода необходимо внимательно
изучить каждое из условий.
switch
, наоборот, предполагает, что условия - это всего лишь конкретные
варианты одного и того же фундаментального сравнения. Они не отличаются друг от
друга по смыслу, только по значению.
Тесты, слишком сильно привязанные к коду, - это боль. Первым шагом в освоении мастерства написания тестов должно быть понимание того, как писать тесты, проверяющие, что делает код, но не знающие того, как он это делает.
Из этого следует, что логике не место в тестах. Тесты должны быть конкретными и очень точными. Следует сопротивляться соблазну включить логику в тесты, потому что эта логика скорее всего будет переиспользовать или даже дублировать логику продуктивного кода, а значит сильно привяжет тесты к текущей реализации.
3. Поиск абстракций
Обязательно следовать OCP при изменениях кода
Безопасный рефакторинг опирается на юнит-тесты. Важный момент - тесты должны подтверждать неизменность поведения, см. Назначение модульных тестов.
При рефакторинге меняется только внутренняя структура кода, но не его наблюдаемое поведение. Поэтому существующие тесты должны проходить при каждом небольшом изменении в процессе рефакторинга.
Если тесты вдруг падают, то это может означать одно из двух:
- Последние внесенные правки изменили видимое поведение кода - нужно откатить правки и подумать, каким образом добиться неизменности видимого поведения.
- Тесты слишком привязаны к реализации, знают слишком много о том, как именно работает продуктивный код. В этом случае необходимо сначала доработать тесты (рефакторинг тестов).
Правила стаи
- Определите наиболее похожие фрагменты кода
- Найдите мельчайшее различие между ними
- Внесите простейшее изменение, устраняющее это различие:
- напишите новый код в объеме, достаточном для компиляции, скомпилируйте
- дополните новый код так, чтобы его можно было запустить, запустите
- дополните новый и старый код так, чтобы использовать результат работы нового кода, запустите
- удалите старый неиспользуемый код
Отличия фрагментов кода как источник абстракций
Очень часто сложные проблемы сложны именно потому, что никто еще не разобрался с простыми.
Множество одновременных изменений - это не рефакторинг, а ре хак торинг
5. Разделение ответственностей
Список вопросов, помогающих при разделении ответственностей классов и методов:
- Есть ли методы, имеющие одинаковую форму?
- Принимают ли разные методы аргумент с одним и тем же именем?
- Всегда ли аргументы с одним и тем же именем означают одно и то же?
- В каких местах этого класса можно было бы добавить модификатор доступа
private
? - Если бы вы собирались разделить этот класс на два, где бы вы провели разделительную черту?
- Имеют ли проверки условий в разных методах что-то общее?
- Сколько веток обычно содержится в блоках условных операторов?
- Содержат ли методы какой-либо код кроме ветвления?
- Зависят ли методы от своего аргумента в большей степени, чем от класса в целом?