Почему рефакторинг и Solid идут рука об руку

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

Рефакторинг — это дисциплинированная техника реструктуризации существующего кода без изменения его внешнего поведения. Он не исправляет ошибки или добавляет функции; вместо этого он улучшает внутреннюю структуру, чтобы будущие изменения становились безопаснее и быстрее. Принципы SOLID, введенные Робертом С. Мартином, обеспечивают набор руководящих принципов проектирования, которые при соблюдении дают код, который можно поддерживать, тестировать и устойчив к изменениям. Обе дисциплины являются естественными союзниками. Рефакторинг — это инструмент, который позволяет постепенно привести существующую кодовую базу в соответствие с SOLID, даже когда оригинальный дизайн был далек от идеала.

На практике многие команды разработчиков изо всех сил пытаются применять принципы SOLID задним числом. Оригинальный код может быть монолитным, тесно связанным или заваленным условной логикой. Без систематического подхода усилия по «сделке его SOLID» кажутся подавляющими. Именно здесь сияют методы рефакторинга. Разбивая работу на небольшие, сохраняющие поведение шаги, вы можете постепенно трансформировать кодовую базу, пока она не выровняется с каждым принципом SOLID. В этой статье рассматриваются конкретные методы рефакторинга для каждого из пяти принципов, в комплекте с практическими указаниями о том, что искать и как действовать.

Понимание принципов Solid

Перед погружением в методы рефакторинга, краткий обзор аббревиатуры SOLID задаст основу:

  • Принцип единой ответственности (Single Responsibility Principle, SRP): Класс должен иметь только одну причину для изменения, то есть он должен иметь одну четко определенную ответственность.
  • Открытый/закрытый принцип (OCP): Сущности программного обеспечения (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации.
  • Лисковский принцип замещения (LSP): Объекты суперкласса должны быть заменяемы объектами подкласса без влияния на правильность программы.
  • Принцип разделения интерфейсов (ISP): Клиенты не должны быть вынуждены зависеть от интерфейсов, которые они не используют.
  • Принцип инверсии зависимости (DIP): модули высокого уровня не должны зависеть от модулей низкого уровня; оба должны зависеть от абстракций.

Каждый принцип касается определенного вида запаха кода. SRP борется с классами, которые «знают слишком много». OCP борется с хрупкими условными цепями. LSP предотвращает хрупкие иерархии наследования. ISP борется с толстыми интерфейсами, которые заставляют бесполезные реализации методов. DIP решает плотную связь с конкретными реализациями. Далее мы смотрим, как рефакторинг может систематически устранять эти запахи.

Рефакторинг для принципа единой ответственности

Выявление нарушений

Наиболее распространенным симптомом нарушения SRP является класс, который имеет более одной причины для изменения. Например, класс под названием , который вычисляет суммы, форматирует счет для отображения, сохраняет его в базе данных и отправляет электронное письмо, имеет по крайней мере четыре обязанности. Любое изменение в расчете налогов, форматировании HTML, схеме хранения или содержании электронной почты заставит измениться в один и тот же класс. Со временем класс становится большим, тесно связанным и трудным для тестирования.

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

Рефакторные методы

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

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

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

Рефакторинг для открытого/закрытого принципа

Замена условностей полиморфизмом

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

Стандартный рефакторинг здесь Заменить условный на полиморфизм. Вы создаете абстрактный базовый класс или интерфейс (например, ) с помощью метода . Каждый способ доставки становится конкретным подклассом. Оригинальный клиентский код затем использует абстракцию, и новые методы доставки добавляются путем создания нового подкласса — открытого для расширения, закрытого для модификации.

Использование стратегии и шаблонов декораторов

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

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

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

Рефакторинг для поддержки принципа замещения Лискова

Подзаголовки и поведенческие контракты

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

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

Использование интерфейсов для обеспечения LSP

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

Другой полезный рефакторинг — это метод «нажать» (FLT:0): если метод в суперклассе имеет смысл только для некоторых подклассов, переместите его вниз к этим подклассам. Это устраняет риск того, что подкласс наследует неподходящий метод. Аналогично, «нажать» (Push Down Field) перемещает состояние, которое не является универсально необходимым.

Реализация сегрегации интерфейсов через рефакторинг

Расщепление толстых интерфейсов

ISP часто нарушается, когда один интерфейс накапливает слишком много методов. Например, интерфейс с , , и заставляет простой текстовый принтер реализовывать методы stub.Рефакторинговое решение — это Extract Interface (или Split Interface) для создания более мелких, более сплоченных интерфейсов: , , и т. д. Каждый клиент тогда зависит только от интерфейсов, которые ему действительно нужны.

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

Рефакторинг существующего клиентского кода

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

Рефакторинг для принципа инверсии зависимостей

Абстрактная зависимость

Типичное нарушение DIP — это класс высокого уровня, такой как , который непосредственно инстанцирует класс низкого уровня, такой как . Это заставляет зависеть от конкретной реализации базы данных, что затрудняет тестирование и замену репозиторием в памяти. Первым рефакторингом является Extract Interface из класса зависимостей: создать интерфейс и заставить реализовать его. Затем изменить , чтобы зависеть от интерфейса. На этом этапе у вас все еще есть скрытое прямое инстанциирование — следующий шаг — Ввести параметр (инъекция конструктора) для прохождения зависимости извне.

Инъекция зависимостей и инверсия контроля

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

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

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

Вывод: Рефакторинг привычки

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

Чтобы углубить свою практику, изучите каталог рефакторингов на веб-сайте Мартина Фаулера Рефакторинг . Для подробного рассмотрения принципов SOLID блог Роберта Мартина Чистый код дает отличные объяснения. Наконец, помните, что рефакторинг без тестов опасен. Всегда убедитесь, что у вас есть солидный набор тестов перед началом; Используйте Метод извлечения и Переименуйте переменную в качестве безопасных первых шагов, когда тесты минимальны. Со временем комбинация дисциплинированного рефакторинга и мышления SOLID превратит ваш код в хорошо структурированный, податливый актив, а не хрупкое обязательство.

Начните с малого: выберите один класс, который нарушает SRP, примените Extract Class и посмотрите, как реагирует остальная часть системы. Уверенность, которую вы получите, мотивирует вас заняться следующим принципом. При последовательной практике вы усвоите эти рефакторинги и начнете проектировать код, который естественным образом уважает SOLID с самого начала.