Фабричный метод
Суть паттерна "Фабричный метод" и его реализация на TypeScript
Фабричный метод — приём из ООП, который позволяет вынести конкретную реализацию объекта в подкласс, оставляя в суперклассе лишь метод для его создания. При этом все созданные объекты должны иметь общий интерфейс.
Зачем это может быть нужно? Чтобы писать код под несколько платформ и не загрязнять класс с бизнес-логикой особенностями реализации и конструкциями if-else.
Какие проблемы решает?
Задача. Есть некая логика асинхронной актуализации данных, которая очень важна для приложения. После её выполнения мы сразу уведомляем юзера о том, что данные успешно обновлены.
Проблема. Бизнес-логика едина, но реализация уведомлений для разных платформ разная.
В коде это может выглядеть как-то так:
type Platform = "firefoxExtension" | "chromeExtension" | "browser";
class UpdateManager {
private platform: Platform;
constructor(platform: Platform) {
this.platform = platform;
}
public update() {
fetch("https://my.app.api/update").then(() => {
// ... какая-то логика работы с полученными данными
// уведомляю, что всё прошло успешно
this.notify("Данные успешно обновлены");
});
}
// в зависимости от платформы
// код для уведомления будет различаться
private notify(message: text) {
if (this.platform === "firefoxExtension") {
browser.notifications.create({
type: "basic",
iconUrl: browser.extension.getURL("icons/success.png"),
title: message,
message: message,
});
}
if (this.platform === "chromeExtension") {
chrome.notifications.create({
type: "basic",
iconUrl: chrome.extension.getURL("icons/success.png"),
title: message,
message: message,
});
}
if (this.platform === "browser") {
alert(message)
}
}
}
Получилось некрасиво и неоптимально.
👎 В коде стало трудней разбираться: бизнес-логика приложения теперь лежит рядом утилитарщиной.
👎 Код класса завязался на глобальные переменные, о которые ему знать не нужно. Даже если мы вынесем нотификацию в отдельный утилитарный класс, это все равно будет неоптимально, ведь мы так или иначе будем вынуждены прокидывать информацию о платформе в низкоуровневые компоненты нашей системы через импорты / конструктор. Таким образом, глобальные знания о приложении размажутся по множеству низкоуровневых компонентов.
Рефакторинг
Оставляем в суперклассе UpdateManager только бизнес-логику. Имплементацию же утилитарной платформозависимой логики отдаём на откуп подклассам. Для этого заводим абстрактный метод, который будет возвращать объект, ответственный за отсылку уведомлений.
// интрфейс класс, ответственного за рассылку уведомлений
interface Notificator {
send: (message: string) => void;
}
// абстрактный класс менеджера уведомлений
abstract class UpdateManager {
public update() {
fetch("https://my.app.api/update")
.then(() => {
// ... какая-то логика работы с полученными данными
// а теперь дай мне объект с интерфейсом Notificator
// как он работает под капотом — мне не важно
const notificator = this.createNotificator();
// уведомляю, что всё прошло успешно
notificator.send("Данные успешно обновлены");
});
}
abstract createNotificator(): Notificator;
}
Используем фабричный метод
Теперь используем новую архитектуру.
interface Notificator {
send: (message: string) => void;
}
abstract class UpdateManager {
public update() {
fetch("https://my.app.api/update").then(() => {
const notificator = this.createNotificator();
notificator.send("Данные успешно обновлены");
});
}
abstract createNotificator(): Notificator;
}
// для расширения Firefox
class FirefoxNotificator implements Notificator {
public send(message: string) {
browser.notifications.create({
type: "basic",
iconUrl: browser.extension.getURL("icons/success.png"),
title: "Готово!",
message: message,
});
}
}
class FirefoxUpdateManager extends UpdateManager {
createNotificator() {
return new FirefoxNotificator();
}
}
// для расширения Chrome
class ChromeNotificator implements Notificator {
public send(message: string) {
chrome.notifications.create(null, {
type: "basic",
iconUrl: "../img/success.png",
title: "Готово!",
message: message,
silent: true,
});
}
}
class ChromeUpdateManager extends UpdateManager {
createNotificator() {
return new ChromeNotificator();
}
}
// для браузера
class BrowserNotificator implements Notificator {
public send(message: string) {
alert(message);
}
}
class BrowserUpdateManager extends UpdateManager {
createNotificator() {
return new BrowserNotificator();
}
}
type Platform = "firefoxExtension" | "chromeExtension" | "browser";
class Application {
private updateManager: UpdateManager;
// знание о платформе переносим на уровень приложения
constructor(platform: Platform) {
if (platform === "chromeExtension") {
this.updateManager = new ChromeUpdateManager();
} else if (platform === "firefoxExtension") {
this.updateManager = new FirefoxUpdateManager();
} else {
this.updateManager = new BrowserUpdateManager();
}
}
public update() {
this.updateManager.update();
}
}
Что в итоге?
👍 Разделили знания о бизнес-логике и об устройстве API различных платформ.
👍 Заложили фундамент под дальнейшее масштабирование на новые платформы.
👍 Знания о платформе лежат там где нужно — на уровне корня приложения. Они не размазаны по разным классами / функциям.
👎 Добавили больше классов, сделали сам код сложнее.
Может возникнуть логичный вопрос:
А что делать если в приложении функций, реализация которых отличнается в зависимости от платформы, начинает накапливаться много? Добавлять в классы ещё фабричных методов?
В таком случае логичным продолжением будет паттерн Абстрактная фабрика. Он предполагает, что всё семейство объектов для конкретной платформы будет порождать специальный класс. Это будет основной задачей абстрактной фабрики, в отличие от подхода с фабричным методом, где создание новых объектов не является основной функцией класса.
Комментарии