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

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

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

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

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

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

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 {
  floor: Floor
  ceiling: Ceiling
  wall: Wall
  armchair?: Armchair // без кресла в доме прожить можно :)

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

function Builder(): Building {
  const house = new Building(new Ceiling(), new Floor(), new Wall())
  house.armchair = new Armchair()
  return house
}

const house = Builder();


/*где-то в другом месте приложения */

house.armchair = new ModernArmchair()


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

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

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

interface BuildingDependencies {
  injectArmchair(dep: Armchair): void
}

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


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


  injectArmchair(armchair: Armchair) {
    this.armchair: Armchair
  }
}

function Builder(): Building {
  const house = new Building(new Ceiling(), new Floor(), new Wall())
  house.injectArmchair(new Armchair())
  return house
}

const house = Builder();


/*где-то в другом месте приложения */

house.injectArmchair(new ModernArmchair())


Вопросы

Наблюдатель

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

Наблюдатель инвертирует контроль за выполнением программы схожим образом, как это делают обработчики событий в 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 при изменении функциональности подкласса.

Вопросы

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