PatternsBridgeFacadeAdapterProxyComposite
105 Вводное видео
- Мост - построение мостов между двумя различными типизациями классов
- Фасад - скрывает реализацию за собой
- Адаптер - позволяет вставить новый объект в существующий код
- Прокси - позволяет добавить логику перед нашим классом
- Композит - упрощает работу с древовидными структурами кода
- Декоратор
106 Bridge
Проблема: Нам нужно реализовать уведомления, которые будут приходить либо в телегу, либо в whatsup. Так же уведомления могут быть мгновенные, либо отложенные. Расширение логики приведёт к тому, что классов для реализации функционала придётся делать больше в геометрической прогрессии
Мы имеем основной класс NotificationSender и через метод provider взаимодействуем с интерфейсом IProvider, который уже влияет на провайдеров. Это позволяет через композицию решить проблему с реализацией взаимодействия
И вот пример реализации паттерна, где у нас в отправителе сообщений присутствует метод-мост, который опирается на интерфейс провадера. Мы можем обращаться к любому провайдеру ровно потому, что уже имеем определённый интерфейс, через который будет осуществляться взаимодействие
// Интерфейс провайдера, через который будет осуществляться взаимодействие между сендером и провайдерами
interface IProvider {
sendMessage(message: string): void;
connect(config: unknown): void;
disconnect(): void;
}
// Реализация двух провайдеров
class TelegramProvider implements IProvider {
sendMessage(message: string): void {
console.log(message);
}
connect(config: string): void {
console.log(config);
}
disconnect(): void {
console.log('Disconnected TG');
}
}
class WhatsUpProvider implements IProvider {
sendMessage(message: string): void {
console.log(message);
}
connect(config: string): void {
console.log(config);
}
disconnect(): void {
console.log('Disconnected WU');
}
}
// Реализация класса по отравке сообщения
class NotificationSender {
// Принимает определённого провайдера, которые реализованы выше
constructor(private provider: IProvider) {
}
send() {
// И пользуется методами этого провайдера
this.provider.connect('connect');
this.provider.sendMessage('message');
this.provider.disconnect();
}
}
// Реализация отложенного отправления сообщения
class DelayNotificationSender extends NotificationSender {
constructor(provider: IProvider) {
super(provider);
}
// Отправка с задержкой
sendDelayed() {
}
}
// Вызов сендеров
const senderTG = new NotificationSender(new TelegramProvider());
senderTG.send();
const senderWU = new NotificationSender(new WhatsUpProvider());
senderWU.send();
107 Facade
У нас есть реализация большого количества действий при отправке того же сообщения: отправка сообщения на сервер, запись в БД, логирование сообщения, отправка уведомления пользователю
Чтобы легко реализовывать определённые задачи и каждый раз не прописывать все действия вручную, можно создать класс, который скроет всю реализацию и станет прослойкой для реализации быстрой отправки того же сообщения
Реализация паттерна фасада, где класс композиционирует все остальные класса, чтобы предоставить пользователю единый интерфейс для реализации нужной задачи
// Класс, который отвечает за оповещение
class Notify {
send(template: string, to: string) {
console.log(`Отправляю ${template} к ${to}`)
}
}
// Логирование
class Log {
log(message: string) {
console.log(message);
}
}
// Шаблонизатор
class Template {
// Хранилище шаблонов (БД)
private templates = [
{name: 'other', template: '<h1>Шаблон</h1>>'},
];
getByName(name: string) {
return this.templates.find(t => t.name === name);
}
}
// И сам фасад, который скрывает все остальные методы, чтобы с ними не работать
class NotificationFacade {
private notify: Notify;
private logger: Log;
private template: Template;
constructor() {
this.notify = new Notify();
this.template = new Template();
this.logger = new Log();
}
send(to: string, templateName: string) {
const data = this.template.getByName(templateName);
if (!data) {
this.logger.log('Не найден шаблон');
return;
} this.notify.send(data.template, to);
this.logger.log('Шаблон отправлен');
}
}
// Использование
const s = new NotificationFacade();
s.send('a@a.ru', 'other'); // Есть только этот метод
108 Adapter
Паттерн Адаптер позволяет подготовить сходу неподходящий объект к использованию в нашем коде. Самый простой пример из жизни: нам нужно воткнуть USB 3.0 в Type-C. Сделать это просто так не получится и поэтому нам нужно использовать переходик - адаптер.
Пример: нам нужно адаптировать все вызовы KVDatabase к персистентной БД
Решается проблема через адаптор, который расширяется от нашей БД и через конструктор прокидывается к нашей персистентной БД
class KVDatabase {
private db: Map<string, string> = new Map();
save(key: string, value: string) {
this.db.set(key, value);
}
}
class PersistentDB {
savePersistent(data: Object) {
console.log(data);
}
}
// Сам адаптер
class PersistentDBAdapter extends KVDatabase {
// Конструктор принимает в себя ту ДБ, под которую мы адаптируемся
constructor(public database: PersistentDB) {
super();
}
override save(key: string, value: string): void {
this.database.savePersistent({ key, value});
}
}
// Создаём функцию, которая умеет работать только с KVDatabase
function run(base: KVDatabase) {
base.save('key', 'myValue')
}
// Код работает, так как PersistentDBAdapter экстендит PersistentDB
run(new PersistentDBAdapter(new PersistentDB()));
Этот паттерн используется для адаптации приложения к работе с другими, внешними, полезными библиотеками, которые приложение по умолчанию не сможет поддерживать
109 Proxy
Паттерн прокси позволяет нам настроить доступность к определённым участкам кода и к определённой функциональности.
У нас есть определённое АПИ для работы с платежами. Мы можем с ним работать из кода. Однако перед нами встаёт задача, что нам нужно ограничить возможность работы с АПИ, чтобы управлять доступом к нему.
Решить проблему мы можем через внедрение зависимости от PaymentAPIProxy и влиять на АПИ платежей через этот прокси. И в этом прокси мы можем кэшировать запросы, проверять доступ к этим запросам и т.д.
// Интерфейс АПИ оплаты
interface IPaymentAPI {
getPaymentDetail(id: number): IPaymentDetail | undefined;
}
// Интерфейс данных оплаты
interface IPaymentDetail {
id: number;
sum: number;
}
// АПИ оплаты
class PaymentAPI implements IPaymentAPI {
private data = [{ id: 1, sum: 10000}];
// Кэширование
getPaymentDetail(id: number): IPaymentDetail | undefined {
return this.data.find(d => d.id === id);
}
}
// Это прокси, который ограничит получение данных
class PaymentAccessProxy implements IPaymentAPI {
constructor(private api: PaymentAPI, private userId: number) {
}
// Получение данных о платеже
getPaymentDetail(id: number): IPaymentDetail | undefined {
if (this.userId === 1) {
return this.api.getPaymentDetail(id);
} console.log('Попытка получить данные платежа!');
return undefined;
}
}
// Пользователь, который может получить данные
const proxyAccess = new PaymentAccessProxy(new PaymentAPI(), 1);
console.log(proxyAccess.getPaymentDetail(1));
// И пользователь, которому уже не доступны данные
const proxyUnaccess = new PaymentAccessProxy(new PaymentAPI(), 2);
console.log(proxyUnaccess.getPaymentDetail(1));
Этот паттерн может быть полезен, когда нам нужно добавить дополнительные настройки логики над существующим функционалом в программе
110 Composite
Паттерн композит позволяет нам объединить большие блоки кода в такую последовательность, где мы сможем от родителей элементов проводить воздействие на эти компоненты
Пример: нам нужно получить общую стоимость товаров. В магазине может быть как один небольшой товар, так и сразу группа товаров
Решить нашу проблему мы можем через создание отдельного интерфейса, который будет хранить в себе общие методы для получения информации из всех элементов. То есть он нам даст одинаковые функции получения стоимости для всех сущностей-родителей
// Наш абстрактный класс, который определяет методы всем его дочерним элементам
abstract class DeliveryItem {
// Массив корзины
item: DeliveryItem[] = [];
// Добавить элемент в корзину
addItem(item:DeliveryItem) {
this.item.push(item);
}
// Получение цены из корзины
getItemPrices(): number {
return this.item.reduce((acc: number, item: DeliveryItem) => {
return acc += item.getPrice()
}, 0);
}
// У каждого элемента своя реализация получения цены
abstract getPrice(): number;
}
// Магазин
class DeliveryShop extends DeliveryItem {
constructor(private deliveryFee: number) {
super();
}
getPrice(): number {
return this.getItemPrices() + this.deliveryFee;
}
}
// Упаковка для товаров - корзина
class Package extends DeliveryItem {
getPrice(): number {
return this.getItemPrices();
}
}
// Единичный продукт
class Product extends DeliveryItem {
// В который передаём цену
constructor(private price: number) {
super();
}
// И который возвращает цену в родительский элемент
getPrice(): number {
return this.price;
}
}
// Создаём магазин товаров
const shop = new DeliveryShop(100);
shop.addItem(new Product(1000));
// Создаём первую корзину товаров
const pack1 = new Package();
pack1.addItem(new Product(200));
pack1.addItem(new Product(300));
shop.addItem(pack1);
// Создаём вторую корзину товаров const pack2 = new Package();
pack2.addItem(new Product(30));
shop.addItem(pack2);
// И просто аккумулируем все цены в одном магазине
console.log(shop.getPrice()); // 1630
Данный паттерн используется тогда, когда нам нужно работать с древовидными структурами, вложенными друг в друга