Хаммер-дроны, или фабричный метод в действии

—

от автора

Любите ли вы комиксы так, как люблю их я? Если нет, то вы просто неправильно их смотрите! Переписать сюжет в виде программного кода — и отдых, и развлечение, и возможность потренироваться. 

Всем привет, это снова Макс Кравец из Holyweb, и сегодня мы будем косплеить Ивана Ванко, то есть делать дронов. Много дронов. Для этого нам понадобится целая фабрика. Поехали!

Делаем классическую фабрику

Итак, жили-были… нет, это все сразу пропускаем, и переходим к моменту, когда Джастин Хаммер уже выкрал Ивана по кличке Хлыст (Whiplash) и притащил «этот дикий рюсский» к себе на производство.

― Чтобы войти в систему, нужно сгенерировать шифрованные пароли. Мы можем сгенерировать пароли?

― Да, сэр.

― Добудь нам… шифрованные пароли…

Продолжать фразу не будем, благо и ломать софт Хаммер Индастриз нам не требуется. Мы просто создадим класс нашего клиентского кода.

/**  * корпорация HummerInc  * наш клиентский код  */ class HummerInc { // **** }

И условно хакнем его, получив доступ ко всему содержимому (да, я знаю, что его пока нет, но всему свое время).

/**  * доступ к возможностям корпорации Хаммер  */ const WhiplashHackHummerInc = new HummerInc();

— Нет, постой-постой-постой. А что они могут делать? Это все-таки демонстрация оружия.

― Могут отдать честь.

Ну просто готовое ТЗ для разработчика! А кроме того, хоть Иван Ванко своему работодателю о том не сообщил, — они умеют перемещаться в пространстве и стрелять.

Для начала — зададим абстрактного дрона.

/**  * описываем абстрактного дрона  */ interface AbstractDrone {   salute(): void;    primaryMove(): void;    primaryShooting(): void; }

и напишем класс для первого типа дронов — пехоты. 

/**  * класс пехоты  */ class TacticalAssault implements AbstractDrone {    salute(): void {     console.log(`Пехотинец отдал честь`);   }    primaryMove(): void {     console.log(`Пехотинец побежал`);   }    primaryShooting(): void {     console.log(`Пехотинец выстрелил из ручного оружия`);   } }

Отлично! Давайте создадим нашего первого бойца и попросим его отдать честь.

const TacticalAssault01 = new TacticalAssault(); TacticalAssault01.salute();
Пехотинец отдал честь

В чем тут проблема? В том, что пока что у нас — ручное производство. Мы прямо указываем, что мы хотим получить нового дрона, применяя new. А если нам этих дронов нужна целая армия? Да еще и делать их нам надо в разных местах кода? Давайте вспоминать — у нас же есть целая корпорация Hammer Inc, вот пусть и работает на полную мощность!

/**  * корпорация HummerInc  * наша фабрика по производству дронов  */ class HummerInc {    /**    * метод, изготавливающий одного дрона    */   createDrone(): AbstractDrone {     return new TacticalAssault();   } }  ...  WhiplashHackHummerInc.createDrone().salute();
Пехотинец отдал честь

Прекрасно, наша корпорация произвела нам пехотинца. Но нам же еще нужны и другие рода войск? Добавляем.

/**  * класс морпехов  */ class SeaAssault implements AbstractDrone {    salute(): void {     console.log(`Морпех отдал честь`);   }    primaryMove(): void {     console.log(`Морпех пошел`);   }    primaryShooting(): void {     console.log(`Морпех выстрелил из гранатомета`);   } }  /**  * класс артиллерии  */ class GroundAssault implements AbstractDrone {    salute(): void {     console.log(`Артиллерист отдал честь`);   }    primaryMove(): void {     console.log(`Артиллерист пошел`);   }    primaryShooting(): void {     console.log(`Артиллерист выстрелил из пушки`);   } }  /**  * класс авиации  */ class AirAssault implements AbstractDrone {    salute(): void {     console.log(`Летчик отдал честь`);   }    primaryMove(): void {     console.log(`Летчик полетел`);   }    primaryShooting(): void {     console.log(`Летчик выпустил ракету`);   } }

Но вот проблема. Метод, производящий дронов, у нас всего один, и делать для каждого типа отдельный не хочется категорически. Не хочется — не будем! Воспользуемся фабричным методом. Для этого нам понадобится немного переписать класс нашей корпорации, добавив в метод createDrone() возможные варианты:

/**  * корпорация HummerInc  * наша фабрика по производству дронов  */ class HummerInc {    /**    * Фабричный метод изготовления дрона нужного нам типа    */   createDrone(droneType:                 'TacticalAssault' |                 'SeaAssault' |                 'GroundAssault' |                 'AirAssault'): AbstractDrone {     switch (droneType) {       case 'TacticalAssault':         return new TacticalAssault();       case 'SeaAssault':         return new SeaAssault();       case 'GroundAssault':         return new GroundAssault();       case 'AirAssault':         return new AirAssault();       default:         throw new Error('Такого дрона мы не производим');     }   } }

Это классический вариант реализации фабричного метода, позволяющий наглядно разобраться в его логике: мы задаем возможные варианты, и для каждого из них с помощью switch-case указываем, какого конкретно дрона мы хотим получить. 

WhiplashHackHummerInc.createDrone('TacticalAssault').salute(); WhiplashHackHummerInc.createDrone('AirAssault').salute();
Пехотинец отдал честь Летчик отдал честь

Наша фабрика работает, позволяя нам производить именно тех дронов, которых мы заказывали. Можно послать Джастина Хаммера… на презентацию.

Код варианта в Гисте.

Делаем настраиваемую фабрику

Реализация с помощью switch-case хороша для понимания паттерна, но слишком жесткая для практического применения. Если нам потребуется добавить новый тип дрона, или, наоборот, избавиться от уже существующего, — в таком варианте придется править исходный код, а это явно не best practice.

Для того чтобы добавить гибкости, воспользуемся сочетанием абстрактной фабрики и коллекции Map():

/**  * Добавим Map() конфигурации, в котором   * будем хранить возможные варианты дронов  */ const droneVariants = new Map();

Перепишем фабричный метод в классе нашей корпорации с использованием стрелочной функции, возвращающей из нашей конфигурации требуемый тип дрона:

/**  * корпорация HummerInc  * наша фабрика по производству дронов,  * вариант, достаточно гибкий для практического использования  */ class HummerInc {   /**    * Перепишем фабричный метод изготовления дрона нужного нам типа    * с использованием конфигурации.    */   createDrone(droneType: new () => AbstractDrone): AbstractDrone {     /**      * Не забудем поставить проверку, возвращающую ошибку      * в случае запроса не существующей модели дрона      */     if (!droneVariants.has(droneType)) {       throw new Error('Такого дрона мы пока не производим, обратитесь к Ивану Ванко');     }     /**      * достаем нужный нам тип дрона из конфигурации      * и возвращаем новый инстанс      */     const droneTypeConstructor = droneVariants.get(droneType);     return new droneTypeConstructor();   } }

Тут у нас возникает небольшая заминка — мы указываем в сигнатуре AbstractDrone, а это у нас всего лишь интерфейс. Исправим это.

/**  * Перепишем интерфейс абстрактного дрона  * на абстрактный класс,предусмотрев возвращение ошибки  * в случае если какой-то метод не реализован в конкретном  * типе дрона  */ abstract class AbstractDrone {   salute(): void {     throw new Error('Метод salute не реализован');   }    primaryMove(): void {     throw new Error('Метод primaryMove не реализован');   }    primaryShooting(): void {     throw new Error('Метод primaryShooting не реализован');   } }

Нам осталось только поправить код реализации конкретных дронов, заменив имплементацию интерфейса на наследование абстрактного класса, и внести возможные варианты в конфигурацию.

/**  * Определим класс пехоты, наследуя его от абстрактного  */ class TacticalAssault extends AbstractDrone {    salute(): void {     console.log(`Пехотинец отдал честь`);   }    primaryMove(): void {     console.log(`Пехотинец побежал`);   }    primaryShooting(): void {     console.log(`Пехотинец выстрелил из ручного оружия`);   } }  /**  * Добавим пехотинца в конфигурацию, указав что такой вариант возможен  */ droneVariants.set(TacticalAssault, TacticalAssault);  /**  * Определим класс морпехов, наследуя его от абстрактного  */ class SeaAssault extends AbstractDrone {    salute(): void {     console.log(`Морпех отдал честь`);   }    primaryMove(): void {     console.log(`Морпех пошел`);   }    primaryShooting(): void {     console.log(`Морпех выстрелил из гранатомета`);   } }  /**  * Добавим морпехов в конфигурацию, указав что такой вариант возможен  */ droneVariants.set(SeaAssault, SeaAssault);  /**  * Определим класс артиллерии, наследуя его от абстрактного  */ class GroundAssault extends AbstractDrone {    salute(): void {     console.log(`Артиллерист отдал честь`);   }    primaryMove(): void {     console.log(`Артиллерист пошел`);   }    primaryShooting(): void {     console.log(`Артиллерист выстрелил из пушки`);   } }  /**  * Добавим артиллерию в конфигурацию, указав что такой вариант возможен  */ droneVariants.set(GroundAssault, GroundAssault);  /**  * Определим класс авиации, наследуя его от абстрактного  */ class AirAssault extends AbstractDrone {    salute(): void {     console.log(`Летчик отдал честь`);   }    primaryMove(): void {     console.log(`Летчик полетел`);   }    primaryShooting(): void {     console.log(`Летчик выпустил ракету`);   } }  /**  * Добавим авиацию в конфигурацию, указав что такой вариант возможен  */ droneVariants.set(AirAssault, AirAssault);

Проверим, работает ли наша новая фабрика?

/**  * доступ к возможностям корпорации Хаммер  */ const WhiplashHackHummerInc = new HummerInc();  WhiplashHackHummerInc.createDrone(TacticalAssault).salute(); WhiplashHackHummerInc.createDrone(AirAssault).salute();
Пехотинец отдал честь Летчик отдал честь

Поговорим о практических вещах

Абстрактные примеры это прекрасно, но что нам дает паттерн Фабрика и зачем он нужен? 

Ну, во-первых, это красиво (С) 🙂

Мы можем добавлять новые реализации дронов, не вмешиваясь в уже написанный код, добавлять и убирать их, просто меняя Map конфигурации. Такой подход выглядит SOLIDно.

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

/**  * Определим класс SomeDrone, наследуя его от абстрактного  */ class SomeDrone extends AbstractDrone {    salute(): void {     console.log(`SomeDrone отдал честь`);   }    primaryMove(): void {     console.log(`SomeDrone полетел`);   }    primaryShooting(): void {     console.log(`SomeDrone выпустил ракету`);   } }  /**  * Добавим SomeDrone в конфигурацию,   * указав что такой вариант возможен  */ droneVariants.set(SomeDrone, SomeDrone);  WhiplashHackHummerInc.createDrone(SomeDrone).salute();
SomeDrone отдал честь
/**  * Нам понадобился новый тип дронов SomeDrone,  * мы его реализовали и использовали в коде,  * но в какой-то момент мы решили вернуться к авиации  */ droneVariants.set(SomeDrone, AirAssault);  WhiplashHackHummerInc.createDrone(SomeDrone).salute();
Летчик отдал честь

Несколько лет тому назад именно паттерн Фабрика был основным способом управления зависимостями, но сегодня этот вариант используется редко — в больших приложениях более удобен вариант Dependency Injection, который мы уже рассматривали в предыдущей статье.

Однако появление более удобного инструмента управления зависимостями вовсе не означает, что от Фабрики надо отказаться.

Используем фабрику для передачи параметров в конструктор

Представьте себе ситуацию: вы пишете библиотеку, которую будут использовать в своих проектах другие разработчики. Разумеется, на вход этой библиотеки понадобится подавать набор каких-то аргументов.

/**  * Наша библиотека принимает на вход два аргумента.  * Стандартный способ передачи аргументов неудобен,   * поскольку мы вынуждены полагаться на неизвестного  * нам разработчика, писать ему инструкцию   * о порядке аргументов и т.д  */ class MyLib {   constructor(arg1, arg2) {     this.arg1 = arg1;     this.arg2 = arg2;   }    render() {     return this.arg1 + " " + this.arg2;   } }  /**  * Фабрика помогает формализовать передачу параметров  * в конструктор нашей библиотеки и делает ее  * использование более прозрачным */ const myLibFactory = (args) => new MyLib(args.arg1, args.arg2); const phrase = myLibFactoryFactory({  arg1: "Hello",  arg2: "World"  });

Используем фабрику для гарантированного создания правильного инстанса

Давайте взглянем на стандартный кусочек кода из документации NestJS.

... /**  * Bootstrap backend-api app  */ async function bootstrap() {   const app = await NestFactory.create(AppModule)   const globalPrefix = 'api'   app.setGlobalPrefix(globalPrefix)   const port = process.env.API_PORT || 3333   await app.listen(port, () => {     Logger.log('Listening at http://localhost:' + port + '/' + globalPrefix)   }) }  bootstrap()

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

Добавляем фабрику в DI

Angular-разработчикам хорошо знакома реализация механизма провайдинга зависимостей в этом фреймворке.

const heroServiceFactory = (logger: Logger, userService: UserService) => {   return new HeroService(logger, userService.user.isAuthorized); };  export let heroServiceProvider =   { provide: HeroService,     useFactory: heroServiceFactory,     deps: [Logger, UserService]   };

С помощью фабрики создатели Angular защищают DI от некорректных значений, которые могут быть переданы в механизм провайдинга.

Давайте подведем итоги

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

WhiplashHackHummerInc.createDrone(SeaAssault).primaryShooting();

С вами был Иван Ванко Макс Кравец из Holyweb, до новых встреч со всеми, кто переживет эту ночь 🙂 

Предложения, претензии, пожелания — пишите в комментарии или сразу мне в Телеграм. 

Другие наши статьи о JS и паттернах проектирования:


ссылка на оригинал статьи https://habr.com/ru/articles/570260/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *