Упражнение по улучшению унаследованного кода

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

Проблема

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





Предположим, у нас есть существующий процесс.

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

Запрошено новое требование:

Чтобы иметь возможность видеть в процессе производства данные об эффективности каждой стратегии в режиме реального времени.

Развязка системы

Посмотрим на точку входа в процесс:

… Контролируемый класс обучения:

и вызываемый метод:

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

Для измерения покрытия воспользуемся техникой мутационного тестирования.

К сожалению, только один тест не проходит, поэтому мы обнаружили, что процесс не описан, и мы видим, что принцип Майкла Фезерса, к сожалению, применяется:

«Унаследованная система - это система, в которой нет тестов»

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

1 - Создание отложенных тестов.

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

Однако мы не можем выполнить рефакторинг для написания тестов, не написав их заранее. Похоже, перед нами замкнутый круг.

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

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

2 - Мы пишем тесты, чтобы охватить уже существующие функции.

Тесты могут быть написаны с помощью инструмента из семейства xUnit с ложным утверждением (они всегда терпят неудачу).

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

3 - Имя класса не представляет собой настоящее имя в биекции.

Помощников не существует в реальном мире, и они не должны существовать в какой-либо вычислимой модели.

Давайте подумаем об обязанностях выбрать имя в MAPPER.



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

4 - Класс одноэлементный.

Нет веских причин для использования синглтонов. Этот факт, помимо создания всех проблем, описанных здесь:



дает очень реальный вызов (связанный с getInstance ()) и не очень декларативный ...

который мы изменим на:

оставив определение класса следующим образом:

Важное правило проектирования:

Не подклассифицируйте конкретные классы.

Если язык позволяет это, мы явно объявляем это:

5 - Одинаковый параметр во всех методах.

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

Это запах кода, предлагающий нам проверить согласованность между этим параметром и процессом.

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



Поэтому мы передадим все важные атрибуты во время строительства.

Чтобы узнать, является ли атрибут существенным, нужно снять с него все обязанности, связанные с этим объектом. Если он больше не может выполнять свои обязанности, это потому, что атрибут принадлежит минимальному набору атрибутов.

Таким образом, стратегия неизменна по своей сути со всеми преимуществами, которые она приносит нам.



6 - Находим шаблон дизайна.

Согласно биекции, этот процесс моделирует реальный мировой процесс. Похоже, это соответствует шаблону Команда.

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

7 - Взаимозаменяемое поведение напоминает еще один образец.

Как следует из названия, присвоенного объекту в соответствии с его обязанностями, этот процесс моделирует стратегию выполнения, которая будет конкурировать с другими полиморфными стратегиями.

Это цель паттерна стратегии.

Имена должны соответствовать наблюдаемым обязанностям.



8 - Исключаем нули.

Нет веской причины использовать null. Null не существует в реальной жизни.

Это нарушает принцип взаимного соответствия и создает связь между вызывающей функцией и аргументом. Кроме того, он генерирует ненужные ifs, поскольку null не полиморфен с каким-либо другим объектом.

Мы меняем отсутствие аргумента на логическое значение истинности.



9 - Убираем параметры по умолчанию.

Частная функция в предыдущем примере имеет параметр по умолчанию.

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



10 - Удаляем жестко запрограммированные константы.

Эти константы, объединенные в коде, не позволят нам делать хорошие тесты, «манипулирующие временем».

Помните, что тесты должны контролировать всю среду, а время должно быть глобальным и нестабильным, чтобы соответствовать тестам.

Отныне он будет неотъемлемым параметром создания объекта (Рефакторинг путем добавления параметров - безопасная задача, которую может выполнить любая современная IDE.



11 - Развязываем бревно.

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

Эта новая связь не дает нам возможности проверить ее. Этот Синглтон находится в другом модуле, над которым мы не можем контролировать, поэтому мы собираемся использовать технику обертывания.

Помимо того, что журнал является синглтоном, он использует сообщения статического класса.

Напомним, что:

Единственный протокол, который должен содержать класс, - это протокол, связанный с его единственной обязанностью (S для Solid): создание экземпляров.

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

Затем мы можем отделить ссылку на журнал и извлечь его из класса, уменьшив связь, создав лучшую согласованность стратегии и повысив ее тестируемость.

Теперь мы можем использовать объект с несколькими разными типами логгеров (например, с тестовыми дублерами).

При вызове из продуктивного кода:

И звонок из тестов:

12 - Мы материализуем предметы.

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

Создавая новую концепцию, мы рискуем построить анемичную модель. Давайте посмотрим, какие скрытые обязанности у вас есть:



13 - Завершаем освещение.

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

И наша система гораздо менее «унаследованная» по сравнению с тем, когда мы ее нашли.

Резюме

После кропотливой итеративной и поэтапной работы, сделав короткие шаги, мы достигли лучшего решения в следующих аспектах:

  • Меньше сцепления.
  • Неизменяемость.
  • Лучшие имена.
  • Нет сеттеров / получателей.
  • Нет, если.
  • Без Null.
  • Без синглтонов.
  • Без параметров по умолчанию.
  • Лучшее тестовое покрытие.
  • Следуя принципу открытого / закрытого (Solid’s O), чтобы иметь возможность добавлять новые полиморфные алгоритмы.
  • Следуя принципу единой ответственности (буква S означает Solid).
  • Без перегрузки классов протоколом.

Выводы

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

Мы даже можем сделать TDD на устаревших системах, используя эти методы.



Частично цель этой серии статей - создать пространство для дебатов и дискуссий по дизайну программного обеспечения.



Ждем комментариев и предложений по этой статье.

Эта статья также доступна на испанском здесь.