В этой серии мы рассмотрим творческие шаблоны проектирования:

  1. Синглтон
  2. Фабричный метод
  3. Абстрактная фабрика
  4. "Опытный образец"
  5. Строитель

❤️ Что это?

С помощью шаблона абстрактной фабрики вы можете создавать группы связанных объектов, не указывая их конкретные классы.
Абстрактная фабрика — это шаблон проектирования, который позволяет создавать группы связанных или зависимых объектов, не сказать, каковы их конкретные классы. Шаблон предоставляет уровень абстракции, который отвечает за создание связанных объектов, а клиентский код использует этот интерфейс для создания объектов. Почему…

🫡 Разберем на примере:

Представьте, что вы делаете игру с такими врагами, как драконы, скелеты, зомби и так далее. У каждого типа (семейства) есть сопутствующие товары (Дракон, Зомби), и эти продукты могут иметь разный уровень сложности.
И мы не хотим изменять уже существующий код, чтобы он не дают нам головную боль каждый раз, когда мы меняем врагов или сложность.

Первое, что говорит шаблон абстрактной фабрики, — это явно объявлять интерфейсы для каждого отдельного продукта в семействе продуктов (например, Zombie, Skeleton или Dragon). Затем вы можете убедиться, что все версии продукта используют одни и те же интерфейсы. Например, все варианты Skeleton могут реализовывать интерфейс Skeleton, все варианты Zombie могут реализовывать интерфейс Zombie и так далее.

Следующий этап включает в себя создание интерфейса Abstract Factory со списком методов создания для всех продуктов в семействе продуктов, таких как createZombie, createDragon и createSkeleton. Эти методы должны возвращать абстрактные типы продуктов, такие как Skeleton, Zombie, Dragon и т. д., которые показаны уже извлеченными интерфейсами.
А как насчет различных видов продуктов? На основе интерфейса AbstractFactory мы делаем отдельный класс фабрики для каждого типа семейства продуктов. Класс, производящий продукцию определенного вида, называется фабрикой. Например, RegularEnemyFactory может создавать только объекты Skeleton и Zombie.

Клиентский код должен иметь возможность общаться как с фабриками, так и с продуктами через их абстрактные интерфейсы. Это позволяет вам изменить тип фабрики, которую вы передаете клиентскому коду, а также вариант продукта, который клиентский код получает, не нарушая самого клиентского кода.
Допустим, клиент просит фабрику сделать скелет. Клиенту не обязательно знать, какой Скелет он получает с завода, и неважно, что это за завод. Клиент должен относиться ко всем скелетам одинаково, используя абстрактный интерфейс скелета. Это верно независимо от того, сделан ли Скелет из песка или из огня. При использовании этого метода клиент знает о Skeleton только одно: что он каким-то образом реализует метод атаки. Кроме того, какой бы вариант скелета ни возвращался, он всегда будет соответствовать типу таблицы зомби или дракона, созданной одним и тем же фабричным объектом.

Говорить дешево, покажи мне код

// Abstract Product Interfaces
interface Enemy {
  attack(): void;
}

interface Dragon extends Enemy {
  breatheFire(): void;
}
interface Skeleton extends Enemy {
  raiseShield(): void;
}
interface Zombie extends Enemy {
  infect(): void;
}
// Abstract Factory Interface
interface EnemyFactory {
  createDragon(): Dragon;
  createSkeleton(): Skeleton;
  createZombie(): Zombie;
}
// Concrete Factory 1
class RegularEnemyFactory implements EnemyFactory {
  createDragon(): Dragon {
    return new RegularDragon();
  }
  createSkeleton(): Skeleton {
    return new RegularSkeleton();
  }
  createZombie(): Zombie {
    return new RegularZombie();
  }
}
// Concrete Factory 2
class BossEnemyFactory implements EnemyFactory {
  createDragon(): Dragon {
    return new BossDragon();
  }
  createSkeleton(): Skeleton {
    return new BossSkeleton();
  }
  createZombie(): Zombie {
    return new BossZombie();
  }
}
// Concrete Product 1
class RegularDragon implements Dragon {
  attack() {
    console.log("Regular Dragon attacks!");
  }
  breatheFire() {
    console.log("Regular Dragon breathes fire!");
  }
}
// Concrete Product 2
class RegularSkeleton implements Skeleton {
  attack() {
    console.log("Regular Skeleton attacks!");
  }
  raiseShield() {
    console.log("Regular Skeleton raises its shield!");
  }
}
// Concrete Product 3
class RegularZombie implements Zombie {
  attack() {
    console.log("Regular Zombie attacks!");
  }
  infect() {
    console.log("Regular Zombie infects its target!");
  }
}
// Concrete Product 4
class BossDragon implements Dragon {
  attack() {
    console.log("Boss Dragon attacks!");
  }
  breatheFire() {
    console.log("Boss Dragon breathes fire!");
  }
}
// Concrete Product 5
class BossSkeleton implements Skeleton {
  attack() {
    console.log("Boss Skeleton attacks!");
  }
  raiseShield() {
    console.log("Boss Skeleton raises its shield!");
  }
}
// Concrete Product 6
class BossZombie implements Zombie {
  attack() {
    console.log("Boss Zombie attacks!");
  }
  infect() {
    console.log("Boss Zombie infects its target!");
  }
}
// Client Code
function createEnemy(factory: EnemyFactory, enemyType: string) {
  let enemy: Enemy;
  switch (enemyType) {
    case "dragon":
      enemy = factory.createDragon();
      break;
    case "skeleton":
      enemy = factory.createSkeleton();
      break;
    case "zombie":
      enemy = factory.createZombie();
      break;
    default:
      throw new Error("Invalid enemy type!");
  }
  return enemy;
}
const regularFactory = new RegularEnemyFactory();
const bossFactory = new BossEnemyFactory();
const regularSkeleton = createEnemy(regularFactory, "skeleton");
regularSkeleton.attack(); // Regular Skeleton attacks!
regularSkeleton.raiseShield(); // Regular Skeleton raises its shield!
const bossZombie = createEnemy(bossFactory, "zombie");
bossZombie.attack(); // Boss Zombie attacks!
bossZombie.infect(); // Boss Zombie infects its target!

Другой пример:

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

Для этого вы можете использовать шаблон абстрактной фабрики. Во-первых, вы должны определить набор интерфейсов для различных типов компонентов пользовательского интерфейса, которые необходимо создать, таких как Button и TextField. Эти интерфейсы будут определять методы и свойства, которые должны быть во всех реализациях компонента.

Затем вы должны определить абстрактный интерфейс фабрики, который имеет методы для создания экземпляров каждого из необходимых вам компонентов пользовательского интерфейса. Например, у вас может быть интерфейс UIFactory с такими методами, как createButton и createTextField. Каждый метод будет возвращать экземпляр соответствующего интерфейса компонента.

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

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

// Button interface
interface Button {
  text: string;
  onClick: () => void;
}

// TextField interface
interface TextField {
  value: string;
  onChange: (value: string) => void;
}
// Abstract factory interface
interface UIFactory {
  createButton(): Button;
  createTextField(): TextField;
}
// Concrete Material UI factory
class MaterialUIFactory implements UIFactory {
  createButton() {
    return {
      text: 'Material Button',
      onClick: () => console.log('Material button clicked')
    };
  }
  createTextField() {
    return {
      value: '',
      onChange: (value: string) => console.log(`Material text field changed to ${value}`)
    };
  }
}
// Concrete iOS UI factory
class IOSUIFactory implements UIFactory {
  createButton() {
    return {
      text: 'iOS Button',
      onClick: () => console.log('iOS button clicked')
    };
  }
  createTextField() {
    return {
      value: '',
      onChange: (value: string) => console.log(`iOS text field changed to ${value}`)
    };
  }
}
// Client code
function createUI(factory: UIFactory) {
  const button = factory.createButton();
  const textField = factory.createTextField();
  return { button, textField };
}
// Example usage
const materialUI = createUI(new MaterialUIFactory());
console.log(materialUI.button.text); // "Material Button"
const iosUI = createUI(new IOSUIFactory());
console.log(iosUI.textField.value); // "" (empty string)

🎨 Структура

🍔 Как это работает

Шаблон Abstract Factory состоит из следующих компонентов:

  • Абстрактная фабрика: это интерфейс, который определяет контракт для создания объектов. Он объявляет набор методов, которые создают связанные объекты.
  • Concrete Factory: это класс, реализующий интерфейс Abstract Factory. Он обеспечивает реализацию для создания связанных объектов.
  • Абстрактный продукт: это интерфейс, который определяет контракт для продуктов, которые создают фабрики.
  • Конкретный продукт: это класс, который реализует интерфейс абстрактного продукта. Он обеспечивает реализацию продуктов, которые создают фабрики.
  • Клиент: это код, который использует фабрики для создания объектов.

Интерфейс Abstract Factory используется клиентским кодом для создания объектов. Абстрактная фабрика создает объекты, сообщая бетонной фабрике, что нужно запускать правильные методы. Объекты производятся Бетонным заводом путем вызова правильных методов в Бетонном изделии.

🏋🏻 Когда использовать Абстрактную фабрику:

  1. Используйте абстрактную фабрику, когда ваш код должен работать с различными семействами связанных продуктов, но вы не хотите, чтобы он зависел от конкретных классов этих продуктов. Это может быть связано с тем, что вы еще не знаете, что это за конкретные классы, или потому, что вы хотите освободить место для будущих дополнений.
  2. Вам нужно создать группы связанных объектов, не говоря, каковы их конкретные классы.
  3. Вы хотите, чтобы клиентский код и конкретные классы были слабо связаны.
  4. Вы хотите, чтобы было легко переключаться между различными группами объектов во время работы программы.

✅ Преимущества

Шаблон «Абстрактная фабрика» имеет ряд преимуществ, в том числе:

  • Он позволяет создавать семейства связанных объектов без указания их конкретных классов.
  • Это способствует слабой связи между клиентским кодом и конкретными классами.
  • Это позволяет легко переключаться между различными семействами объектов во время выполнения.

❌ Недостатки

Шаблон Abstract Factory имеет некоторые недостатки, в том числе:

  • Это может привести к сложному коду, особенно когда имеется много семейств связанных объектов.
  • Расширение шаблона для поддержки новых семейств объектов может быть затруднено.

🌪️ Заключение

Шаблон Abstract Factory позволяет создавать связанные объекты без указания их конкретных классов. Это способствует ослаблению связи и упрощает переключение между различными семействами объектов во время выполнения.

Дополнительные пояснения и примеры кода можно найти в моем репозитории:



Надеюсь, вам понравилась статья ❤️