Давайте рассмотрим паттерн проектирования Абстрактная фабрика C#, для чего он нужен и какие проблемы он решает. Где можно применять шаблон Abstract Factory C#, а где это будет излишним.
Идея паттерна Абстрактная фабрика C#
Паттерн (шаблон) проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте рассмотрим один из наиболее часто используемых порождающих паттернов — Абстрактная фабрика (Abstract Factory).
Как я уже писал ранее, существует три вида паттернов проектирования:
- Порождающие паттерны позволяют возможность выполнять инициализацию объектов наиболее удобным и оптимальным способом.
- Структурные паттерны описывают взаимоотношения между различными классами или объектами, позволяя им совместно реализовывать поставленную задачу.
- Поведенческие паттерны позволяют грамотно организовать связь между сущностями для оптимизации и упрощения их взаимодействия.
Абстрактная фабрика (Abstract Factory) – это порождающий паттерн, предоставляющий возможность создания семейства взаимосвязанных или родственных объектов, не специфицируя их классов. То есть, мы определяем интерфейс для создания взаимосвязанных объектов, без необходимости реализации конкретных классов.
Архитектура паттерна Abstract Factory
Давайте рассмотрим UML диаграмму абстрактной фабрики.
- AbstractFactory — объявляет интерфейс для создания семейства взаимосвязанных или родственных объектов
- AbstractProductA, AbstractProductB — семейство продуктов, которые будут использоваться клиентом для выполнения своих задач
- ProductA1, ProductB1 — конкретные типы продуктов
- Client — клиент фабрики, который получает конкретные продукты для реализации своего поведения
Логика работы паттерна абстрактная фабрика
Паттерн абстрактная фабрика позволяет создавать группы взаимосвязанных объектов. То есть для начала мы должны определить группу свойств и методов, которые будут характерны для всех представителей группы. Например возможность передвигаться у живых существ. Потом мы можем выделить конкретные фабрики для производства разных групп существ, например фабрику по производству птиц, которые будут летать, фабрику для млекопитающих, которые будут бегать, и фабрику пресмыкающихся, которые будут ползать. Каждая из фабрик будет создавать экземпляры своего типа, но при этом все они будут иметь возможность передвигаться, но по своему.
Реализация паттерна проектирования Абстрактная фабрика (Abstract Factory) на языке C#
Итак, для начала выберем предметную область для которой будем разрабатывать абстрактную фабрику. Пусть это будет невероятно упрощенное подобие пошаговой стратегии «Космические рейнджеры». Мы будем использовать абстрактную фабрику для создания экземпляров космических кораблей. Итак, класс абстрактной фабрики будет выглядеть следующим образом:
SpaceshipFactoryBase.cs
namespace Patterns.AbstractFactory { /// <summary> /// Класс описывающий Абстрактную фабрику. /// </summary> /// <remarks> /// Описываются семейства взаимосвязанных классов для создания космического корабля. /// </remarks> public abstract class SpaceshipFactoryBase { /// <summary> /// Запас здоровья космического корабля. /// </summary> public int Health { get; protected set; } = 300; /// <summary> /// Тип космического корабля. /// </summary> public string Type { get; protected set; } = "Космический корабль"; /// <summary> /// Создать двигатель космического корабля. /// </summary> /// <returns>Двигатель.</returns> public abstract EngineBase CreateEngine(); /// <summary> /// Создать оружие космического корабля. /// </summary> /// <returns>Оружие.</returns> public abstract GunBase CreateGun(); /// <summary> /// Создать источник энергии космического корабля. /// </summary> /// <returns>Источник энергии.</returns> public abstract EnergyBase CreateEnergy(); } }
Как видно из кода, у любого космического корабля, создаваемого с помощью абстрактной фабрики будут два свойства Уровень здоровья и Тип, а также будут динамически определиться его Двигатель, Оружие и Источник энергии.
Теперь нам необходимо рассмотреть реализацию абстрактных компонентов, из которых состоит космический корабль Двигатель, Оружие и Источник энергии.
EnergyBase.cs
namespace Patterns.AbstractFactory { /// <summary> /// Базовый абстрактный класс описывающий возможности источников энергии космических кораблей. /// </summary> public abstract class EnergyBase { /// <summary> /// Количество оставшейся энергии источнике. /// </summary> public int Volume { get; protected set; } /// <summary> /// Использовать энергию из источника. /// </summary> /// <param name="volume">Количество потребляемой энергии.</param> /// <returns>Оставшаяся энергия в источнике.</returns> public abstract int Using(int volume); } }
Как видно из кода, у двигатель любой источник энергии космического корабля содержит в себе одно свойство Количество энергии и метод, позволяющий эту энергию расходовать. Но при этом это никакой-то определенный источник энергии, это только шаблон описывающий каким должен быть источник энергии.
Соответственно, теперь нам нужно для разнообразия определить несколько возможных источников энергии:
SunEnergy.cs
using System; namespace Patterns.AbstractFactory { /// <summary> /// Источник энергии на основе солнечной радиации. /// </summary> public class SunEnergy : EnergyBase { /// <inheritdoc /> public override int Using(int volume) { if (volume < 0) { throw new ArgumentException("Расход топлива не может быть отрицательным.", nameof(volume)); } // Пусть это будет идеальный бесконечный источник энергии. return Volume; } /// <summary> /// Создать экземпляр источника энергии на основе солнечной радиации. /// </summary> public SunEnergy() { Volume = 100; } } }
PlasmEnergy.cs
using System; namespace Patterns.AbstractFactory { /// <summary> /// Источник энергии на основе плазмы. /// </summary> public class PlasmEnergy : EnergyBase { /// <inheritdoc /> public override int Using(int volume) { if (volume < 0) { throw new ArgumentException("Расход топлива не может быть отрицательным.", nameof(volume)); } // Обычный расход энергии при использовании. if (Volume >= volume) { Volume -= volume; return Volume; } else { return 0; } } /// <summary> /// Создать экземпляр источника энергии на основе плазмы. /// </summary> public PlasmEnergy() { Volume = 100; } } }
Мы создали два возможных источника энергии (солнечный и плазменный), которые уже конкретно реализуют логику расходования энергии.
Таким же образом нам необходимо реализовать и шаблоны для оружия и двигателей, и их конкретные реализации.
GunBase
namespace Patterns.AbstractFactory { /// <summary> /// Базовый абстрактный класс, описывающий возможности оружия космических кораблей. /// </summary> public abstract class GunBase { /// <summary> /// Максимально возможная дистанция между комическими кораблями для выстрела из оружия. /// </summary> public int Distance { get; protected set; } /// <summary> /// Выстрелить из оружия. /// </summary> /// <returns>Количество нанесенного урона.</returns> public abstract int Shoot(); } }
LaserGun.cs
namespace Patterns.AbstractFactory { /// <summary> /// Лазерная пушка. /// </summary> public class LaserGun : GunBase { /// <inheritdoc /> public override int Shoot() { // Стреляет не сильно, но стабильно return 30; } /// <summary> /// Создать экземпляр лазерной пушки. /// </summary> public LaserGun() { Distance = 100; } } }
PhotonGun.cs
using System; namespace Patterns.AbstractFactory { /// <summary> /// Фотонная пушка. /// </summary> public class PhotonGun : GunBase { /// <summary> /// Генератор случайных чисел. /// </summary> private Random _random = new Random(); /// <summary> /// Минимально возможный урон. /// </summary> private readonly int _minDmg = 10; /// <summary> /// Максимально возможный урон. /// </summary> private readonly int _maxDmg = 80; /// <summary> /// Вероятность осечки. /// </summary> private readonly int _missСhance = 10; /// <inheritdoc /> public override int Shoot() { // Не стабильный урон, может сильно выстрелить, может слабо, а может вовсе не выстрелить, но стреляет на большей дистанции. var miss = _random.Next(0, 100); if (miss < _missСhance) { return 0; } var dmg = _random.Next(_minDmg, _maxDmg); return dmg; } /// <summary> /// Создать экземпляр фотонной пушки. /// </summary> public PhotonGun() { Distance = 300; } } }
EngineBase.cs
using System; namespace Patterns.AbstractFactory { /// <summary> /// Базовый абстрактный класс, описывающий возможности двигателей космических кораблей. /// </summary> public abstract class EngineBase { /// <summary> /// Количество используемой энергии. /// </summary> public int UsingEnergy { get; protected set; } = 1; /// <summary> /// Выполнить полет. /// </summary> /// <param name="energy">Используемый для полета источник энергии.</param> /// <returns>Расстояние, на которое выполнился перелет.</returns> public virtual int Move(EnergyBase energy) { if(energy == null) { throw new ArgumentNullException(nameof(energy)); } energy.Using(UsingEnergy); return 1; } } }
PhotonEngine.cs
using System; namespace Patterns.AbstractFactory { /// <summary> /// Фотонный двигатель. /// </summary> public class PhotonEngine : EngineBase { /// <summary> /// Генератор случайных чисел. /// </summary> private Random _random = new Random(); /// <summary> /// Максимальный множитель скорости. /// </summary> private readonly int _maxFactor = 10; public override int Move(EnergyBase energy) { if(energy == null) { throw new ArgumentNullException(nameof(energy)); } // Очень нестабильный, но потенциально быстрый двигатель. // Потребляет случайное количество энергии. int factorEnergy = _random.Next(0, _maxFactor); energy.Using(UsingEnergy * factorEnergy); // Движется со случайной скоростью или вообще останавливается. int factorSpeed = _random.Next(0, _maxFactor); return UsingEnergy * factorSpeed; } /// <summary> /// Создать экземпляр фотонного двигателя. /// </summary> public PhotonEngine() { UsingEnergy = 3; } } }
PulseEngine.cs
using System; namespace Patterns.AbstractFactory { /// <summary> /// Импульсный двигатель. /// </summary> public class PulseEngine : EngineBase { /// <summary> /// Множитель скорости полета. /// </summary> private readonly int _speedFactor = 5; /// <inheritdoc /> public override int Move(EnergyBase energy) { if (energy == null) { throw new ArgumentNullException(nameof(energy)); } // Стандартный режим полета. Не очень быстро, но стабильно. var baseSpeed = base.Move(energy); return baseSpeed * _speedFactor; } /// <summary> /// Создать экземпляр импульсного двигателя. /// </summary> public PulseEngine() { } } }
Таким образом мы получаем три класса шаблона для каждой составляющей двигателя и по две конкретных реализации.
Теперь нам необходимо создать конкретные фабрики по производству заданных типов космических кораблей. То есть это заранее заложенная конфигурация компонентов и других свойств, заданные в фабрике, которая будет создавать экземпляры классов в соответствии с шаблоном. Для начала опишем пиратский корабль.
PiratSpaceshipFactory.cs
namespace Patterns.AbstractFactory { /// <summary> /// Пиратский космический корабль. /// </summary> public class PiratSpaceshipFactory : SpaceshipFactoryBase { /// <summary> /// Создать экземпляр пиратского космического корабля. /// </summary> public PiratSpaceshipFactory() { Health = 200; Type = "Пиратский корабль"; } /// <summary> /// Создаем источник энергии пиратского комического корабля. /// </summary> /// <returns>Плазменный источник энергии.</returns> public override EnergyBase CreateEnergy() { return new PlasmEnergy(); } /// <summary> /// Создаем оружие пиратского комического корабля. /// </summary> /// <returns>Фононная пушка.</returns> public override GunBase CreateGun() { return new PhotonGun(); } /// <summary> /// Создаем двигатель пиратского космического корабля. /// </summary> /// <returns>Фотонный двигатель.</returns> public override EngineBase CreateEngine() { return new PhotonEngine(); } } }
Как видно из кода здесь мы переопределяем абстрактные компоненты космического корабля конкретными типами, а также устанавливаем некоторые свойства.
Теперь добавим другой тип космических кораблей. Пусть это будут военные корабли.
WarSpaceshipFactory.cs
namespace Patterns.AbstractFactory { /// <summary> /// Военный космический корабль. /// </summary> public class WarSpaceshipFactory : SpaceshipFactoryBase { /// <summary> /// Создать экземпляр военного космического корабля. /// </summary> public WarSpaceshipFactory() { Health = 500; Type = "Военное судно"; } /// <summary> /// Создать источник энергии военного космического корабля. /// </summary> /// <returns>Плазменный двигатель.</returns> public override EnergyBase CreateEnergy() { return new PlasmEnergy(); } /// <summary> /// Создать оружие военного космического корабля. /// </summary> /// <returns>Лазерная пушка.</returns> public override GunBase CreateGun() { return new LaserGun(); } /// <summary> /// Создать двигатель военного космического корабля. /// </summary> /// <returns>Импульсный двигатель.</returns> public override EngineBase CreateEngine() { return new PulseEngine(); } } }
Соответственно мы переопределяем те же компоненты абстрактной фабрики, но другой конфигурацией.
Таким образом у нас существует абстрактная фабрика, описывающая структуру космического корабля без конкретного описания из чего он состоит (у корабля должно быть оружие, но какое не конкретизируется). Далее у нас есть фабрики, которые конкретизируют некоторые компоненты космического корабля (у пиратских кораблей стоят фотонные пушки). И теперь осталось описать класс космического корабля, который будет выходить из любой фабрики.
Spaceship.cs
using System; namespace Patterns.AbstractFactory { /// <summary> /// Космический корабль. /// </summary> public class Spaceship { /// <summary> /// Название космического корабля. /// </summary> public string Name { get; private set; } /// <summary> /// Тип космического корабля. /// </summary> public string Type { get; private set; } /// <summary> /// Запас здоровья космического корабля. /// </summary> public int Health { get; private set; } /// <summary> /// Источник энергии космического корабля. /// </summary> private EnergyBase _energy; /// <summary> /// Оружие космического корабля. /// </summary> private GunBase _gun; /// <summary> /// Двигатель космического корабля. /// </summary> private EngineBase _engine; /// <summary> /// Создать экземпляр космического корабля. /// </summary> /// <param name="name">Название корабля.</param> /// <param name="factory">Фабрика, создающая космический корабль.</param> public Spaceship(string name, SpaceshipFactoryBase factory) { if(string.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } if(factory == null) { throw new ArgumentNullException(nameof(factory)); } Name = name; Type = factory.Type; Health = factory.Health; _energy = factory.CreateEnergy(); _gun = factory.CreateGun(); _engine = factory.CreateEngine(); } /// <summary> /// Выстрелить. /// </summary> /// <returns>Нанесенный урон.</returns> public int Shoot() { return _gun.Shoot(); } /// <summary> /// Лететь. /// </summary> /// <returns>Преодоленное расстояние.</returns> public int Move() { return _engine.Move(_energy); } /// <summary> /// Получить урон. /// </summary> /// <param name="damage">Величина урона.</param> public void TakeDamage(int damage) { Health -= damage; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns>Полное название космического корабля.</returns> public override string ToString() { return $"{Type} \"{Name}\""; } } }
Соответственно, при создании корабля нам достаточно передать его имя и фабрику для его создания. Такая реализация дает возможность очень быстро адаптировать конфигурации кораблей, добавлять новые типы компонентов, создавать новые фабрики для различных видов космических кораблей.
Теперь нам необходимо проверить работу нашей фабрики. Давайте устроим соревнование между пиратским кораблем и военным судном. Соревноваться они будут в дальности полета и в нанесении урона. Для удобства использования создадим отдельный класс соревнования.
Battle.cs
using System; namespace Patterns.AbstractFactory { /// <summary> /// Класс-контроллер отвечающий за проведение боев между космическими кораблями. /// </summary> public class Battle { /// <summary> /// Первый космический корабль. /// </summary> private Spaceship _ship1; /// <summary> /// Второй космический корабль. /// </summary> private Spaceship _ship2; /// <summary> /// Создать экземпляр битвы. /// </summary> /// <param name="ship1">Первый космический корабль.</param> /// <param name="ship2">Второй космический корабль.</param> public Battle(Spaceship ship1, Spaceship ship2) { _ship1 = ship1 ?? throw new ArgumentNullException("Первый космический корабль не может быть пустым.", nameof(ship1)); _ship2 = ship2 ?? throw new ArgumentNullException("Второй космический корабль не может быть пустым.", nameof(ship2)); } /// <summary> /// Бой между космическими кораблями. /// </summary> /// <returns>Победивший в бою корабль. null - если оба корабля погибли.</returns> public Spaceship Fight() { // Сражаемся насмерть, пока у одного из космических кораблей не закончится здоровье. while (_ship1.Health > 0 && _ship2.Health > 0) { _ship2.TakeDamage(_ship1.Shoot()); _ship1.TakeDamage(_ship2.Shoot()); } // Выявляем победителя if (_ship1.Health > 0) { return _ship1; } if (_ship2.Health > 0) { return _ship2; } return null; } /// <summary> /// Гонка между космическими кораблями. /// </summary> /// <returns>Победивший в гонке космический корабль. /// null - если оба пролетели одинаковое расстояние. /// </returns> public Spaceship Race() { // Переменные для подсчета дистанции пройденной кораблями. int length1 = 0; int length2 = 0; // Оба корабля летят на протяжении 100 ходов. for (int i = 0; i < 100; i++) { length1 += _ship1.Move(); length2 += _ship2.Move(); } // Выявляем победителя. if (length1 > length2) { return _ship1; } if (length2 > length1) { return _ship2; } return null; } } }
Ну и теперь нам осталось только использовать наши классы и вывести результаты пользователю.
Program.cs
static void Main(string[] args) { Console.WriteLine("Добро пожаловать на 76-ые Космические игры..."); Console.ReadLine(); var piratShip = new Spaceship("Навуходоносор", new PiratSpaceshipFactory()); var warShip = new Spaceship("Ностромо", new WarSpaceshipFactory()); Console.WriteLine($"Первый претендент: {piratShip}"); Console.WriteLine($"Второй претендент: {warShip}"); Console.ReadLine(); var battle = new Battle(piratShip, warShip); Console.WriteLine("Поехали!"); Console.ReadLine(); var raceWinner = battle.Race(); if(raceWinner != null) { Console.WriteLine($"Поприветствуйте победителя гонки {raceWinner}"); } else { Console.WriteLine("В гонке ничья"); } Console.ReadLine(); Console.WriteLine("Да начнется смертельная битва!"); Console.ReadLine(); var battleWinner = battle.Fight(); if (battleWinner != null) { Console.WriteLine($"Поприветствуйте победителя сражения {battleWinner}"); } else { Console.WriteLine("В битве ничья"); } Console.ReadLine(); }
В итоге получаем следующий результат работы программы:
Полный исходный код программы и реализация паттерна абстрактная фабрика C# можно найти в репозитории https://github.com/shwanoff/AbstractFactory
Рекомендую также изучить статью Паттерн Фабричный метод (Factory Method). А еще подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.