Шаблоны проектирования и приёмы рефакторинга
Соблюдать принцип открытости-закрытости помогают несколько шаблонов проектирования и приёмов рефакторинга.
Абстрактная фабрика
Фабрика — это сущность, которая создаёт другие сущности по заданным правилам, например, создаёт экземпляры классов или объекты.
Абстрактная фабрика — это фабрика, которая создаёт фабрики.
Этот шаблон позволяет создавать сущности без привязки к конкретным классам — то есть без привязки к реализации. Так добавление новых типов сущностей изменяет минимальное количество модулей.
Рассмотрим применение абстрактной фабрики на том же примере с отчётом, что был у нас в разделе об SRP.
В этом примере работа для абстрактной фабрики — выбор класса, который будет заниматься форматированием.
// часть с интерфейсом Formatter и классами,
// реализующими его, остаётся такой же
// добавляется новый интерфейс, описывающий фабрику фабрик
interface FormatterFactory {
createFormatter(data: ReportData): Formatter
}
// метод createFormatter возвращает абстрактный интерфейс,
// поэтому обе фабрики ниже взаимозаменяемы
class HtmlFormatterFactory implements FormatterFactory {
createFormatter(data: ReportData) {
return new HtmlFormatter(data)
}
}
class TxtFormatterFactory implements FormatterFactory {
createFormatter(data: ReportData) {
return new TxtFormatter(data)
}
}
// при конфигурации приложение выберет нужный тип фабрики и будет работать с ним;
// коду приложения при этом будет не важно, с какой фабрикой он будет работать,
// потому что он будет зависеть от интерфейсов, а не от конкретных классов
class AppConfigurator {
reportType: ReportTypes
constructor(reportType: ReportTypes) {
this.reportType = reportType
}
configure(reportData: ReportData): FormatterFactory {
if (this.reportType === ReportTypes.Html) {
return new HtmlFormatterFactory(reportData)
}
else return new TxtFormatterFactory(reportData)
}
}
Вопросы
Стратегия
В прошлом примере мы избавились от необходимости менять код форматеров при добавлении новых требований. Но вы могли заметить, что в методе configure
класса AppConfigurator
есть условие, которое проверяет тип формата для отчёта.
По-хорошему, подобные условия следует заменять на динамический выбор нужных сущностей. С этим может помочь ещё один шаблон — стратегия.
Этот шаблон позволяет менять настройки, конфигурацию или алгоритм в зависимости от ситуации и требований. В нашем случае стратегии мы отдадим выбор необходимой фабрики:
function formatterStrategy(reportType: ReportTypes): FormatterFactory {
const formatters = {
[ReportTypes.Html]: HtmlFormatterFactory,
[ReportTypes.Txt]: TxtFormatterFactory,
default: TxtFormatterFactory
}
return formatters[reportType] || formatters.default
}
Теперь выбор фабрик стал динамическим, и при изменении требований нам потребуется только добавить новую сущность и обновить список фабрик.
Вопросы
Декоратор
Декоратор — это шаблон, который заключается в создании обёрток с дополнительной функциональностью для объектов. Такие обёртки позволяют не изменять сам объект, но при этом расширять его функциональность.
Отличие декоратора от наследования в возможности расширять функциональность динамически, без необходимости описывать каждый класс-наследник отдельно.
В примере мы создаём класс BaseGreeting
, метод greet
которого будет выводить строку с приветствием пользователя. Декоратор GreetingWithUppercase
будет приводить строку с приветствием к верхнему регистру.
interface Greeting {
username: string
greet(): string
}
// базовая функциональность описывается в классе,
// который будет обёрнут с помощью декораторов
class BaseGreeting implements Greeting {
username: string
constructor(username: string) {
this.username = username
}
greet(): string {
return `Hello, ${this.username}!`
}
}
// wrappee — объект, функциональность которого мы будем расширять
interface GreetingDecorator {
wrappee: Greeting
greet(): string
}
class GreetingWithUppercase implements GreetingDecorator {
wrappee: Greeting
constructor(wrappee: Greeting) {
this.wrappee = wrappee
}
greet(): string {
// 1. используем базовое поведение
const baseGreeting = this.wrappee.greet()
// 2. расширяем его
return baseGreeting.toUpperCase()
}
}
Сама философия этого шаблона повторяет принцип открытости-закрытости — мы не меняем базовый класс, а добавляем сущности, которые содержат в себе изменения бизнес-требований.
Разница между декоратором и прокси в том, что прокси всегда предоставляет тот же интерфейс, в то время как декоратор может предоставлять расширенный интерфейс.
Вопросы
Наблюдатель
Наблюдатель — шаблон, который создаёт механизм подписки, когда некоторые сущности могут реагировать на поведение других.
В примере ниже у нас есть класс SoftwareEngineerApplicant
, который реагирует на появление вакансий в экземпляре класса HrAgency
.
// интерфейс соискателя описывает его имя и желаемую позицию
interface Applicant {
name: string
position: string
}
// интерфейс наблюдателя описывает метод,
// который будет вызываться наблюдаемым объектом при наступлении события
interface Observer {
update(data: any): void
}
interface NewPositionObserver extends Applicant, Observer {
update(position: string): void
}
class SoftwareEngineerApplicant implements NewPositionObserver {
name: string
position: string
constructor(name: string, position: string) {
this.name = name
this.position = position
}
update(position: string) {
if (position !== this.position) return
console.log(`Hello! My name is ${this.name}, I would like to apply to a ${position} position`)
}
}
// базовый интерфейс наблюдаемого объекта
interface Observable {
subscribe(observer: Observer): void
unsubscribe(observer: Observer): void
notify(data: any): void
}
// наблюдаемый объект хранит в себе список наблюдателей,
// которых будет уведомлять о наступлении события
interface NewPositionObservable extends Observable {
listeners: NewPositionObserver[]
notify(position: string): void
}
class HrAgency implements NewPositionObservable {
listeners: NewPositionObserver[] = []
subscribe(applicant: NewPositionObserver): void {
this.listeners.push(applicant)
}
unsubscribe(applicant: NewPositionObserver): void {
this.listeners = this.listeners.filter(listener =>
listener.name !== applicant.name)
}
// уведомляем всех наблюдателей, когда появляется новая вакансия
notify(position: string): void {
this.listeners.forEach(listener => {
listener.update(position)
})
}
}
Этот шаблон позволяет добавлять новых наблюдателей без необходимости менять код наблюдаемого объекта.
Вопросы
Замена прямого наследования на полиморфизм и композицию
Модули, зависящие от конкретных классов, связаны с ними слишком сильно, из-за чего изменение в одном модуле затронет изменение в другом.
Замена прямого наследования на полиморфизм или композицию — это приём рефакторинга, который ослабляет зацепление модулей через введение абстракций: интерфейсов, обёрток и т. д.
Примером может послужить пример из раздела «В идеальном мире», где мы вводили прослойку из интерфейса Shape
. Когда класс AreaCalculator
начинает зависеть от интерфейса, а не от конкретных классов, изменение в требованиях не затрагивает код класса AreaCalculator
.
Вопросы
Материалы к разделу
- Composition over inheritance
- Фабрика
- Абстрактная фабрика
- Реализация абстрактной фабрики на TypeScript
- Стратегия
- Фабричный метод и применение стратегии
- Реализация стратегии на TypeScript
- Декоратор
- Decorator and Factory, Colorado Computer Science, PDF
- Реализация декоратора на TypeScript
- Наблюдатель
- Реализация наблюдателя на TypeScript
- Example of Just-in-time design: refatoring to OCP
- Refactor to the Open-Closed, PDF