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

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

Комментарии (9)


  1. Capsmol
    29.07.2021 14:59
    +1

    Лучшее сочетание, JS + Marvel !


  1. Felan
    29.07.2021 20:49

    А где фабричный метод то?.

    Суть фабрики или фабричного метода в том, что класс возвращаемого объекта зависит от фактического класса объекта фабрики. А тут обычный кейс внутри. Это уж сервис локатор тогда какой-то и то я не уверен.


    1. Maxim_from_HW Автор
      30.07.2021 15:21

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

      Суть умения рассказать что-то — декомпозировать сложное до простого, упростить до состояния, понятного без дополнительного контекста.

      Суть статьи — показать логику реализации, откуда растут ноги этой задачи в игровой, простой форме.

      Суть процесса изучения — понять принцип и углубиться в вопрос самостоятельно.

      Суть комментариев — найти недосказанное автором)) При желании — можно найти и сервис локатор (только не в свич-кейсе, а в мапе), но в рамках темы статьи — это фигура умолчания.


      1. Felan
        02.08.2021 17:27

        Суть фабрики, а так же фабричного метода вы понимаете не правильно. Вот для примера: https://refactoring.guru/design-patterns/abstract-factory
        Обратите внимание, единственное место где есть if-else - это место где выбирается фабрика. И в этом суть шаблона. Это порождающий шаблон, который "развязывает" источник объектов и множество типов, которые источник выдает. И не просто развязывает, а выносит решение о том, что фабрика будет создавать в design time, у вас же выбор делает "фабрика" и в run time. По сути у вас простой case для создания объектов разных классов. Это называется обычный полиморфизм.

        Суть умения рассказывать оценить тут не берусь, ибо говорите одно а пишите другое.

        Суть статьи обсуждать безсмысленно, т.к. заявлен фабричный метод, а примеры к паттерну отношения не имеют.

        Суть процесса изучения - разбираться как правильно, а не мешать в кучу концепции.

        У вас фабрика принимает решение о том, что инстанциировать, а она не должна это делать. Каждая фабрика/фабричный метод делает что-то одно. Выбор происходит по средством подстановки конкретного типа фабрики\класса с фабричным методом.

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

        Но оставим это на ответственность читателей :)


        1. vbNoName
          10.08.2021 00:32

          Пример по ссылке очень уж далек от практики. Что если мне все фабрики нужны сразу, и выбор нужной при старте не подходит под бизнес логику? Выбор конкретной фабрики будет происходить в run time, а кто все эти фабрики будет порождать? Добро пожаловать switch case по фабрикам?

          И да, классический подход фабрики очень уж противоречивый как по мне. У Вас в фабрике есть тре метода порождающие разные объекты(createChair/createSofa/createCoffeeTable), хорошо что метода только 3 а не 103. А как же open closed principle? При добавлении нового предмета лезть в уже написанный класс? А как же single responsibility? Почему методы создающие диван и кресло в одном классе?


  1. noodles
    31.07.2021 13:19

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

    Не совсем понял - какая разница, править свитч-кейс или структуру Map?


    1. Felan
      02.08.2021 17:46

      Никакой. Там вообще ни свича ни мапы быть не должно. Если мы про фабрику или фабричный метод говорим.


      1. Alexandroppolus
        02.08.2021 19:39

        Свич или мап может быть, но уже при создании самой фабрики - выбор конкретного класса, реализующего какой-нибудь интерфейс ISomeFactory. Хотя даже и там может оказаться фабрика или фабричный метод, тогда свич/мап уедет ещё дальше :) Но где-то он рано или поздно всплывет.


        1. Felan
          02.08.2021 21:17

          Я собственно про это и говорил. Целую телегу накатал постом выше.

          То, что свитч\мап где-то там плавает - неважно. Важно что он к внутренностям паттерна не имеет отношения, и плавает за пределами.

          Весь смысл паттерна, что бы вынести ветвления подальше.