Паттерн “Абстрактная фабрика” (Abstract Factory) предоставляет интерфейс для создания семейств взаимосвязанных объектов с определенными интерфейсами без указания конкретных типов данных объектов.
Когда использовать абстрактную фабрику
- Когда система не должна зависеть от способа создания и компоновки новых объектов
- Когда создаваемые объекты должны использоваться вместе и являются взаимосвязанными
Формальное определение паттерна на языке C# может выглядеть следующим образом:
class AbstractFactory
{
public abstract AbstractProductA CreateProductA();
public abstract AbstractProductB CreateProductB();
}
class ConcreteFactory1: AbstractFactory
{
public override AbstractProductA CreateProductA()
{
return new ProductA1();
}
public override AbstractProductB CreateProductB()
{
return new ProductB1();
}
}
class ConcreteFactory2: AbstractFactory
{
public override AbstractProductA CreateProductA()
{
return new ProductA2();
}
public override AbstractProductB CreateProductB()
{
return new ProductB2();
}
}
abstract class AbstractProductA
{}
abstract class AbstractProductB
{}
class ProductA1: AbstractProductA
{}
class ProductB1: AbstractProductB
{}
class ProductA2: AbstractProductA
{}
class ProductB2: AbstractProductB
{}
class Client
{
private AbstractProductA abstractProductA;
private AbstractProductB abstractProductB;
public Client(AbstractFactory factory)
{
abstractProductB = factory.CreateProductB();
abstractProductA = factory.CreateProductA();
}
public void Run()
{ }
}
Паттерн определяет следующих участников:
- Абстрактные классы AbstractProductA и AbstractProductB определяют интерфейс для классов, объекты которых будут создаваться в программе.
- Конкретные классы ProductA1 / ProductA2 и ProductB1 / ProductB2 представляют конкретную реализацию абстрактных классов
- Абстрактный класс фабрики AbstractFactory определяет методы для создания объектов. Причем методы возвращают абстрактные продукты, а не их конкретные реализации.
- Конкретные классы фабрик ConcreteFactory1 и ConcreteFactory2 реализуют абстрактные методы базового класса и непосредственно определяют какие конкретные продукты использовать
- Класс клиента Client использует класс фабрики для создания объектов. При этом он использует исключительно абстрактный класс фабрики AbstractFactory и абстрактные классы продуктов AbstractProductA и AbstractProductB и никак не зависит от их конкретных реализаций
Посмотрим, как мы можем применить паттерн. Например, мы делаем игру, где пользователь должен управлять некими супергероями, при этом каждый супергерой имеет определенное оружие и определенную модель передвижения. Различные супергерои могут определяться комплексом признаков. Например, эльф может летать и должен стрелять из арбалета, другой супергерой должен бегать и управлять мечом. Таким образом, получается, что сущность оружия и модель передвижения являются взаимосвязанными и используются в комплексе. То есть имеется один из доводов в пользу использования абстрактной фабрики.
И кроме того, наша задача при проектировании игры сделать способ создание абстрагировать создание супергероев от самого класса супергероя, чтобы создать более гибкую архитектуру. И для этого применим абстрактную фабрику:
class Program
{
static void Main(string[] args)
{
Hero elf = new Hero(new ElfFactory());
elf.Hit();
elf.Run();
Hero voin = new Hero(new VoinFactory());
voin.Hit();
voin.Run();
Console.ReadLine();
}
}
//абстрактный класс - оружие
abstract class Weapon
{
public abstract void Hit();
}
// абстрактный класс движение
abstract class Movement
{
public abstract void Move();
}
// класс арбалет
class Arbalet : Weapon
{
public override void Hit()
{
Console.WriteLine("Стреляем из арбалета");
}
}
// класс меч
class Sword : Weapon
{
public override void Hit()
{
Console.WriteLine("Бьем мечом");
}
}
// движение полета
class FlyMovement : Movement
{
public override void Move()
{
Console.WriteLine("Летим");
}
}
// движение - бег
class RunMovement : Movement
{
public override void Move()
{
Console.WriteLine("Бежим");
}
}
// класс абстрактной фабрики
abstract class HeroFactory
{
public abstract Movement CreateMovement();
public abstract Weapon CreateWeapon();
}
// Фабрика создания летящего героя с арбалетом
class ElfFactory : HeroFactory
{
public override Movement CreateMovement()
{
return new FlyMovement();
}
public override Weapon CreateWeapon()
{
return new Arbalet();
}
}
// Фабрика создания бегущего героя с мечом
class VoinFactory : HeroFactory
{
public override Movement CreateMovement()
{
return new RunMovement();
}
public override Weapon CreateWeapon()
{
return new Sword();
}
}
// клиент - сам супергерой
class Hero
{
private Weapon weapon;
private Movement movement;
public Hero(HeroFactory factory)
{
weapon = factory.CreateWeapon();
movement = factory.CreateMovement();
}
public void Run()
{
movement.Move();
}
public void Hit()
{
weapon.Hit();
}
}
Таким образом, создание супергероя абстрагируется от самого класса супергероя. В то же время нельзя не отметить и недостатки шаблона. В частности, если нам захочется добавить в конфигурацию супергероя новый объект, например, тип одежды, то придется переделывать классы фабрик и класс супергероя. Поэтому возможности по расширению в данном паттерне имеют некоторые ограничения.
Реализация шаблона в общем виде
- разрабатываем интерфейсы объектов семейства и Абстрактной фабрики;
- создаем семейства объектов и реализации Абстрактной фабрики для них;
- в программе, например, в зависимости от версии ОС, конфигурации или другого параметра, порождается необходимая реализация Абстрактной фабрики.
- в дальнейшем используется только интерфейсы как Абстрактной фабрики, так и порождаемых ей объектов.
Примеры реализации
1. Стандартный подход
Наглядный пример Абстрактной фабрики это поддержка разных стилей графического интерфейса приложения. Предположим, у нас есть “стандартный” (Defalut GUI) и “раскрашенный” (Skinned GUI) вид окон и элементов управления.
Определим интерфейсы объектов и Абстрактной фабрики (методы и свойства убраны для краткости):
public interface IWindow { }
public interface IButton { }
public interface ITextBox { }
public interface IGUIFactory
{
IWindow CreateWindow();
IButton CreateButton();
ITextBox CreateTextbox();
}
Создадим реализацию каждого семейства и их Абстрактной фабрики:
/// Default GUI
public class DefaultWindow : IWindow { }
public class DefaultButton : IButton { }
public class DefaultTextBox : ITextBox { }
public class DefaultGUIFactory : IGUIFactory
{
public IWindow CreateWindow() { return new DefaultWindow(); }
public IButton CreateButton() { return new DefaultButton(); }
public ITextBox CreateTextbox() { return new DefaultTextBox(); }
}
/// Skinned GUI
public class SkinnedWindow : IWindow { }
public class SkinnedButton : IButton { }
public class SkinnedTextBox : ITextBox { }
public class SkinnedGUIFactory : IGUIFactory
{
public IWindow CreateWindow() { return new SkinnedWindow(); }
public IButton CreateButton() { return new SkinnedButton(); }
public ITextBox CreateTextbox() { return new SkinnedTextBox(); }
}
Все готово и осталось только рассмотреть пример использования. Пусть это будет метод, создающий окно для ввода строки пользователем:
public string GetUserInput(IGUIFactory guiFactory)
{
// Create UI elements
IWindow wndInput = guiFactory.CreateWindow();
IButton btnOk = guiFactory.CreateButton();
IButton btnCancel = guiFactory.CreateButton();
ITextBox tbInput = guiFactory.CreateTextbox();
// TODO: Setup the window and elements
wndInput.AddChild(btnOk);
wndInput.AddChild(btnCancel);
wndInput.AddChild(tbInput);
// TODO: Show dialog
// TODO: Get the result
return tbInput.GetText();
}
При вызове этого метода, необходимо передать ему экземпляр нужной Абстрактной фабрики (DefaultGUIFactory или SkinnedGUIFactory) для получения окна заданного вида.
2. Использование generics (общих типов/шаблонов)
Очень часто процесс создания классов, реализующих один и тот же интерфейс, идентичен. В таких случаях код Абстрактных фабрик будет отличаться только именами классов. Поэтому вполне логично использовать generics. Предыдущий вариант можно переделать вот так:
public class GUIFactoryGeneric<TWindow, TButton, TTextBox> : IGUIFactory
where TWindow : IWindow, new()
where TButton : IButton, new()
where TTextBox : ITextBox, new()
{
public IWindow CreateWindow() { return new TWindow(); }
public IButton CreateButton() { return new TButton(); }
public ITextBox CreateTextbox() { return new TTextBox(); }
}
public class DefaultGUIFactory :
GUIFactoryGeneric<DefaultWindow, DefaultButton, DefaultTextBox> { };
public class SkinnedGUIFactory :
GUIFactoryGeneric<SkinnedWindow, SkinnedButton, SkinnedTextBox> { };
Но есть проблема: данный вариант не может гарантировать, что с помощью GUIFactoryGeneric порождается именно семейство объектов. Что, ну кроме здравого смысла, мешает написать вот такой странный код: GUIFactoryGeneric<SkinnedWindow, DefaultButton, DefaultTextBox>? Хотя, это может быть даже просто опечатка.
Для решения этой проблемы скроем generic-класс внутри статического. А для создания экземпляров Абстрактной Фабрики используем параметризованный Фабричный метод.
public static class GUI
{
private class GUIFactoryGeneric<TWindow, TButton, TTextBox> : IGUIFactory
where TWindow : IWindow, new()
where TButton : IButton, new()
where TTextBox : ITextBox, new()
{
public IWindow CreateWindow() { return new TWindow(); }
public IButton CreateButton() { return new TButton(); }
public ITextBox CreateTextbox() { return new TTextBox(); }
}
public enum Style { Default, Skinned }
public static IGUIFactory GetFactory(Style guiStyle)
{
switch (guiStyle) {
case Style.Default:
return new GUIFactoryGeneric<DefaultWindow, DefaultButton, DefaultTextBox>();
case Style.Skinned:
return new GUIFactoryGeneric<SkinnedWindow, SkinnedButton, SkinnedTextBox>();
}
throw new ArgumentException("An invalid guiStyle: " + guiStyle.ToString());
}
}
Теперь, используя GetFactory(), можно создавать только заданные реализации Абстрактной Фабрики.