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