На главную

Фабричный метод

Суть паттерна "Фабричный метод" и его реализация на TypeScript

Фабричный метод — приём из ООП, который позволяет вынести конкретную реализацию объекта в подкласс, оставляя в суперклассе лишь метод для его создания. При этом все созданные объекты должны иметь общий интерфейс.

Зачем это может быть нужно? Чтобы писать код под несколько платформ и не загрязнять класс с бизнес-логикой особенностями реализации и конструкциями if-else.

Какие проблемы решает?

Условия. Допустим, мы пишем приложение которое, должно работать как на веб-страничке так и в качестве расширения для браузеров Chrome и Firefox.

Задача. Есть некая логика асинхронной актуализации данных, которая очень важна для приложения. После её выполнения мы сразу уведомляем юзера о том, что данные успешно обновлены.

Проблема. Бизнес-логика едина, но реализация уведомлений для разных платформ разная.

В коде это может выглядеть как-то так:

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 различных платформ.

👍 Заложили фундамент под дальнейшее масштабирование на новые платформы.

👍 Знания о платформе лежат там где нужно — на уровне корня приложения. Они не размазаны по разным классами / функциям.

👎 Добавили больше классов, сделали сам код сложнее.

Может возникнуть логичный вопрос:

А что делать если в приложении функций, реализация которых отличнается в зависимости от платформы, начинает накапливаться много? Добавлять в классы ещё фабричных методов?

В таком случае логичным продолжением будет паттерн Абстрактная фабрика. Он предполагает, что всё семейство объектов для конкретной платформы будет порождать специальный класс. Это будет основной задачей абстрактной фабрики, в отличие от подхода с фабричным методом, где создание новых объектов не является основной функцией класса.

Комментарии