Любите ли вы комиксы так, как люблю их я? Если нет, то вы просто неправильно их смотрите! Переписать сюжет в виде программного кода — и отдых, и развлечение, и возможность потренироваться.
Всем привет, это снова Макс Кравец из 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 и паттернах проектирования: