PatternsFactorySingletonBuilder

100 Сами порождающие паттерны

  • Factory - определяет какой-то суперкласс, который внутри позволяет порождать объекты разных классов, что удобно для переиспользования в огромных приложениях
  • Singleton - обеспечивает нам создание единственного класса и его инстанса на всё приложение, откуда бы мы к нему не обращались, что удобно для переиспользования данных внутри или переиспользования всего инстанса
  • Prototype - предлагает делать нам клонирование одного объекта в другой без вдавания во внутреннюю реализацию
  • Builder - говорит нам создавать сложные объекты поэтапно

101 Factory Method

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

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

Правильная реализация фабрики выглядит следующим образом: Мы создаём общий интерфейс, который хранит в себе интерфейс самой страховки. И создаём фабрику, которая хранит в себе логику под каждый вид отдаваемых данных

И вот представление реализации фабрики:

// Статус запроса
enum Status {  
    SUCCESS,
    EXPECTATION,  
    DENY
}  
 
// Базовая реализация страховки
interface IInsurance {  
    id: number;  
    status: Status;  
    setVehicle(vehicle: any): void;  
    submit(): Promise<boolean>;  
}  
 
// Реализация конкретной страховки
class TFInsurance implements IInsurance {  
    id: number;  
    status: Status;  
    private vehicle: any;  
  
    setVehicle(vehicle: any): void {  
        this.vehicle = vehicle;  
    }  
  
    async submit(): Promise<boolean> {  
        const res = await fetch('tf', {  
            method: 'POST',  
            body: JSON.stringify({ vehicle: this.vehicle }),  
        });  
  
        const data = await res.json();  
        return data.isSuccess;  
    }  
}  
 
// Реализация конкретной страховки
class ABInsurance implements IInsurance {  
    id: number;  
    status: Status;  
    private vehicle: any;  
  
    setVehicle(vehicle: any): void {  
        this.vehicle = vehicle;  
    }  
  
    async submit(): Promise<boolean> {  
        const res = await fetch('ab', {  
            method: 'POST',  
            body: JSON.stringify({ vehicle: this.vehicle }),  
        });  
  
        const data = await res.json();  
        return data.yes;  
    }  
}  
  
// Реализация фабрики  -------------
// Создаём абстрактный класс для отображения самой фабрики
abstract class InsuranceFactory {  
    db: any; // данные  
 
	// Создаём нужную страховку по интерфейсу
    abstract createInsurance(): IInsurance;  
 
	// Сохраняем данные страховки
    saveHistory(ins: IInsurance) {  
        this.db.save(ins.id, ins.status);  
    }  
}  
 
// Создаём класс для отделения фабрики одной страховки  
class TFInsuranceFactory extends InsuranceFactory {  
    createInsurance(): TFInsurance {  
        return new TFInsurance();  
    }  
}  
 
// И другой страховки
class ABInsuranceFactory extends InsuranceFactory {  
    createInsurance(): ABInsurance {  
        return new ABInsurance();  
    }  
}  
 
// Использование
const tfInsuranceFactory = new TFInsuranceFactory();  
const ins = tfInsuranceFactory.createInsurance();
tfInsuranceFactory.saveHistory(ins);

Либо мы можем сделать такую реализацию (немного не ООП): Тут мы в класс createInsurance передаём один из ключей, которые представляют из себя типы страховок. Получая этот тип страховки по ключу, мы возвращаем этот тип и определяем его реализацию Хоть тут и гораздо более компактное представление фабрики, однако тут мы теряем возможность дописывать отдельно функциональность для каждого типа страховок

const INSURANCE_TYPE = {  
    tf: TFInsurance,  
    ab: ABInsurance  
}  
  
type IT = typeof INSURANCE_TYPE;  
  
class InsuranceFactoryAlt {  
    db: any;  
  
    createInsurance<T extends keyof IT>(type: T): IT[T] {  
        return INSURANCE_TYPE[type];  
    }  
  
    saveHistory(ins: IInsurance) {  
        this.db.save(ins.id, ins.status);  
    }  
}

102 Singleton

У нас есть одно хранилище и от него нам нужно создать только один инстанс класса - и не давать создавать другие.

По итогу мы имеем сервисы, которые представляют из себя шину для работы с данными, которые находятся внутри хранилища данных

class MyMap {  
    // Тут храним инстанс, созданный один раз  
    private static instance: MyMap;  
  
    map: Map<number, string> = new Map();  
  
    // Запрещаем создание инстансов через конструктор  
    private constructor() {}  
  
    // Очистка мапы  
    clean() {  
        this.map = new Map();  
    }  
  
    public static get(): MyMap {  
        if (!MyMap.instance) {  
            MyMap.instance = new MyMap();  
        }        return MyMap.instance;  
    }  
}  
  
class Service1 {  
    // Добавляем значение в мапу  
    addMap(key: number, value: string): void {  
        const myMap = MyMap.get();  
        myMap.map.set(key, value); // сетим значения  
    }  
}  
  
class Service2 {  
    // Получаем значение из мапы  
    getKeys(key: number): void {  
        const myMap = MyMap.get();  
        console.log(myMap.map.get(key)); // выводим мапу  
        myMap.clean(); // очищаем мапу  
        console.log(myMap.map.get(key)); // выводим очищенную мапу  
    }  
}  
  
new Service1().addMap(1, 'Работает!'); // ввели значение  
new Service2().getKeys(1); // Работает - undefined

103 Prototype

Данный паттерн эксплуатирует возможности JS, а именно его прототипирование. Используется, когда нам нужно склонировать объект и все его данные, изменив внутри только небольшую часть данных

То есть обеспечением клонирования объекта занимается какой-то один метод clone()

Вот пример, где мы создали копию одного и того же пользователя, но с разными почтами

interface Prototype<T> {  
    clone(): T;  
}  
  
class UserHistory implements Prototype<UserHistory> {  
    createdAt: Date;  
  
    constructor(public email: string, public name: string) {  
        this.createdAt = new Date();  
    }  
  
    clone(): UserHistory {  
        let target = new UserHistory(this.email, this.name);  
        target.createdAt = this.createdAt;  
        return target;  
    }  
}  
  
let user = new UserHistory('a@a.ru', 'Антон');  
console.log(user);  
  
let user2 = user.clone();  
user2.email = 'b@b.ru';  
console.log(user2);

104 Builder

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

Основным отличием метода билдера является то, что он чейнебл - его можно вызвать много раз друг за другом. То есть объект вызывает метод build и возвращает изменённого себя

И вот пример билдера, который всегда подгоняет данные под нужную структуру объекта. Это даёт нам возможность создать объект, который мы можем спокойно передавать через АПИ.

// Форматы изображений
enum ImageFormat {  
    PNG = 'png',  
    JPEG = 'jpeg',  
}  
 
// Параметры изображений
interface IResolution {  
    width: number,  
    height: number  
}  
 
// Объединение формата и разрешения изображений
interface IImageConversion extends IResolution {  
    format: ImageFormat  
}  
 
// Класс для создания объекта
class ImageBuilder {  
    // Создаём переменные, которые будут хранить параметры изображений и сразу присваиваем им массив, чтобы мы могли запушить в них данные извне
    private formats: ImageFormat[] = [];  
    private resolutions: IResolution[] = [];  
 
	// Реализация для PNG
    addPng() {  
        // includes - >ES2016  
        if (this.formats.includes(ImageFormat.PNG)) {  
            return this;  
        }        this.formats.push(ImageFormat.PNG);  
        return this;  
    }  
 
	// Реализация для JPEG
    addJpeg() {  
        if (this.formats.includes(ImageFormat.JPEG)) {  
            return this;  
        }        this.formats.push(ImageFormat.JPEG);  
        return this;  
    }  
 
	// Добавляет для изображений параметры размеров
    addResolution(width: number, height: number) {  
        this.resolutions.push({ width, height });  
        return this;  
    }  
 
	// Конечный метод, который преобразует наши данные в нужный формат
    build(): IImageConversion[] {  
        const res: IImageConversion[] = [];  
        for (const r of this.resolutions) {  
            for (const f of this.formats) {  
                res.push({  
                    format: f,  
                    height: r.height,  
                    width: r.width  
                });  
            }  
        }  
        return res;  
    }  
}  
 
// Вызов создания объекта
console.log(  
    new ImageBuilder()  
        .addJpeg()  
        .addPng()  
        .addResolution(100, 50)  
        .addResolution(200, 100)  
        .build()  
)

Когда использовать паттерн?

  • Всегда, когда нам нужно создать сложный объект и не задумываться над его обязательными свойствами (чтобы, например, передать его через АПИ на бэк или фронт)
  • При тестировании приложения