Шаблоны проектирования и приёмы рефакторинга

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

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

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

Инъекция через конструктор

Самый простой вид инъекции — через конструктор. При создании класса в конструкторе мы перечисляем все зависимости, которые требуются для создания экземпляра.

class Building {
  floor: Floor
  ceiling: Ceiling
  wall: Wall

  constructor(floor: Floor, ceiling: Ceiling, wall: Wall) {
    this.floor = floor
    this.ceiling = ceiling
    this.wall = wall
  }
}

Если зависимостей много, перечисление их всех в конструкторе может стать проблемой. Эту проблему частично решает инъекция через сеттер.

Инъекция через сеттер

При таком внедрении каждая зависимость указывается в поле, которое можно изменить через set. Тогда внутри функции Builder вначале мы создаём объект house, а затем устанавливаем зависимости как значения для полей.

class Building {
  public floor: Floor
  public ceiling: Ceiling
  public wall: Wall

  constructor() {/*...*/}
}

function Builder(): Building {
  const house = new Building()
  house.ceiling = new Ceiling()
  house.floor = new Floor()
  house.wall = new Wall()
  return house
}

Проблема этого подхода в том, что поля с зависимостями становятся public, что не всегда приемлемо.

Инъекция с помощью интерфейса

Подход похож на предыдущий, только в нём используются не сеттеры, а отдельные методы-инжекторы. Их мы описываем в интерфейсе BuildingDependencies, который реализует класс Building. Внутри функции Builder мы вызываем инжекторы, передавая как аргумент нужную зависимость.

interface BuildingDependencies {
  injectWall(dep: Wall): void
  injectFloor(dep: Floor): void
  injectCeiling(dep: Ceiling): void
}

class Building implements BuildingDependencies {
  floor: Floor
  ceiling: Ceiling
  wall: Wall

  constructor() {/*...*/}

  injectCeiling(ceiling: Ceiling) {
    this.ceiling = ceiling
  }

  injectFloor(floor: Floor) {
    this.floor = floor
  }

  injectWall(wall: Wall) {
    this.wall = wall
  }
}

function Builder(): Building {
  const house = new Building()
  house.injectCeiling(new Ceiling())
  house.injectFloor(new floor())
  house.injectWall(new Wall())
  return house
}

Вопросы

Наблюдатель

Наблюдатель — шаблон, который создаёт механизм подписки, когда некоторые сущности могут реагировать на поведение других.

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

В примере из раздела об OCP класс SoftwareEngineerApplicant следит за появлением новой вакансии у HrAgency. Метод update решает, как обработать изменение состояния.

Взаимодействие классов SoftwareEngineerApplicant и HrAgency «становится фреймворком», который следит за изменениями и вызывает нужные методы.

Вопросы

Шаблонный метод

Шаблонный метод — это шаблон, который определяет скелет алгоритма, а некоторые шаги даёт реализовывать подклассам. Так подклассы могут переопределять части алгоритма, не меняя общей структуры.

В примере ниже шаблонный метод brewBeverage задаёт каркас алгоритма приготовления напитка.

abstract class BeverageMachine {
  public brewBeverage(): Beverage {
    this.turnOn()
    this.prepareIngredients()
    this.prepareContainer()
    this.brew()
    this.hook()
  }

  // базовые операции имеют реализацию
  public turnOn(): void {
    this.on = true
  }

  // специфичные для каждого подкласса операции
  // будут переопределяться потомками
  abstract public prepareIngredients(): void
  abstract public prepareContainer(): void
  abstract public brew(): void

  // хуки предоставляют дополнительные точки расширения
  // в некоторых критических местах алгоритма;
  // их переопределять не обязательно,
  // так как есть пустая реализация по умолчанию
  public hook(): void {}
}

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

class CoffeeMachine extends BeverageMachine {
  abstract public prepareIngredients(): void {
    this.grindBeans()
    this.heatMilk()
  }

  abstract public prepareContainer(): void {
    this.getNewCup()
  }

  abstract public brew(): void {
    this.pourEspresso()
    this.pourMilk()
  }

  // ...
}

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

Преимущество такого подхода в повторном использовании алгоритма с различными вариациями. Опасность шаблона — в случайном нарушении LSP при изменении функциональности подкласса.

Вопросы

Материалы к разделу