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

Всем привет, это снова Макс Кравец из 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 и паттернах проектирования:
