Шаблоны проектирования и приёмы рефакторинга
Следовать принципу инверсии зависимостей помогают инъекция зависимостей, наблюдатель и шаблонный метод.
Инъекция зависимостей
Внедрять зависимости можно тремя способами: через конструктор, через сеттеры и интерфейсно.
Инъекция через конструктор
Самый простой вид инъекции — через конструктор. При создании класса в конструкторе мы перечисляем все зависимости, которые требуются для создания экземпляра.
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 при изменении функциональности подкласса.