Love Frontend
Статья опубликована: 10.04.2017

Шаблон проектирования Фабрика в ES6

Я пытаюсь получить максимальную отдачу от новшеств в ES6 (ES2015). И я пишу новую библиотеку, в которой мне потребовалась Фабрика. Я заинтересовался, как я могу выгодно использовать новые вещи, чтобы сделать это. В то же время не забывая, что код на ES5 будет также валиден и спокойно используется рядом с ES6

Я искал и гуглил, потому что было много умных кодеров, которые обычно задают вопросы такого типа и я получил умные ответы. Хотя было довольно трудно получить результат и я не мог найти что-нибудь, что объединяло бы обе области: ES6 + Factory-pattern (Шаблон Фабрика). Может быть потому что никто не писал об этом или просто потому что обе области имеют множество ссылок на себя и популярные посты о других технологиях или о ES5 показывались в топе поисковой выдачи и новые посты еще не поднялись в ранге.

Так я добавил что-то от себя. Но прежде, чем погрузится в предмет, нам необходимы общие понимания того, что такое паттерн (шаблон) проектирования Фабрика. Существует 3 понятия шаблона Фабрики:

Простая фабрика (simple Factory)

Простая фабрика - это объект, который инкапсулирует создание другого объекта. В ES6 это может быть конструктором, вызванным new()


//Definition of class
class User {
    constructor(typeOfUser){
        this._canEditEverything = false;
        if (typeOfUser === "administrator") {
            this._canEditEverything = true;
        }
    }
    get canEditEverything() { return this._canEditEverything; }
}
//Instatiation
let u1 = new User("normalGuy");
let u2 = new User("administrator");

Это и есть простая фабрика, как видите, очень простая. Здесь применяется только один класс. Хорошо подходит для множества ситуаций.

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

Фабричный метод определяет один метод, создания экземпляров, который переопределен подклассом, который решает, что вернуть. Фабрики и Продукты должны соответствовать интерфейсу клиентов, чтобы быть доступными для их использования.

Продолжая использовать new вместо создания новых методов, в ES6 это может быть реализовано расширяющимися классами (extend)


//Class
class User {
    constructor(){
        this._canEditEverything = false;
    }
    get canEditEverything() { return this._canEditEverything; }
}
//Sub-class
class Administrator extends User {
    constructor() {
        super();
        this._canEditEverything = true;
    }
}
//Instatiation
let u2 = new Administrator();
u2.canEditEverything; //true

Абстрактная Фабрика - Abstract Factory

Шаблон абстрактная Фабрика предоставляет интерфейс для создания семейства связанных или зависимых объектов без точного определения их конкретных классов.

Важное слово здесь - семейства. Продолжая наш пример это может быть реализовано в ES6 как interface (class) и много конкретных классов (подклассов). В TypeScript (как и в C#) interface существует как определение (без какой-либо реализации) и классы реализуют его. Но в JS (включая ES6) этого нету, это причина по которой мы используем класс и подклассы, которые наследуют его. У нас могут быть подклассы "Administrator", "Editor", "Publisher" и т.д.

Регистрация конкретного объекта на лету (run-time)

Всё идёт нормально, но мне нужно было что-то более полезное. Мне нужно было:

Так я сделал ES6 класс со статичным методом для регистрации и создания классов. Что-то вроде этого:


class Factory {
    constructor() {
    }
    static register(clazzname, clazz) {
        if ((!Factory._registeredTypes.has(clazzname) &&
            clazz.prototype instanceof User)) {
            Factory._registeredTypes.add(clazzname, clazz);
            ...
        } else { ... }
    }
    static create(clazzname, ...options) {
        if (!Factory._registeredTypes.has(clazzName)) {
            console.error("!!!");
            return null;
        }
        let clazz = this._registeredTypes.get(clazzName);
        let instance = new clazz(...options);
        return instance;
    }
}
Factory._registeredTypes = new Map();

Обратите внимание на две вещи: (1) использование метода с типом static и (2) привзяка map() к свойствам внизу. В ES6 вы можете использовать static метод (его не обязательно использовать для создания экземпляра класса), он не будет доступен экземпляру и он не имеет доступа к экземпляру через this. Но тут нету "static" свойств. Таким образом, мы должны создать их в (2).

Это работает хорошо. И кто-то из вас возможно заметит, что это как настоящая реализация паттерна. Но я так не думаю.

Возможно это дело стиля, но я предпочитаю объекты вместо классов или функций в данном случае. В итоге я использовал простой объект:


const Factory = {
    registeredTypes: new Map(),
    register(clazzname, clazz) {
        if (!(Factory.registeredTypes.has(clazzname) &&
            clazz.prototype instanceof User)) {
            Factory._registeredTypes.add(clazzname, clazz);
        } else { ... }
    },
    create(clazzname, ...options) {
        if (!Factory.registeredTypes.has(clazzName)) {
            console.error("!!!");
            return null;
        }
        let clazz = this.registeredTypes.get(clazzName);
        let instance = new clazz(...options);
        return instance;
    }
}

Это всё, после двух дней выяснений как реализовать это в моей новой ES6 библиотеке, я пришел к довольно стандартному решению!