Andrey on .NET | Порождающие шаблоны: Абстрактная фабрика (Abstract factory)

Порождающие шаблоны: Абстрактная фабрика (Abstract factory)

Название шаблона

Абстрактная фабрика (Abstract factory).

Тип

Порождающий шаблон проектирования (Creational).

Описание

Abstract FactoryАбстрактная фабрика предоставляет интерфейс, позволяющий порождать семейства объектов c заданными интерфейсами. При этом их реализации могут варьироваться.

Данный шаблон используется в случае, если:

  • система не должна зависеть от способа создания и реализации входящих в нее объектов;
  • (и) система работает с семействами объектов;
  • (и) входящие в семейство объекты должны использоваться совместно.

Обратите внимание, клиентский код использует в работе только интерфейсы. Реализации Абстрактной фабрики и порождаемых ею объектов скрыты. Такой подход уменьшает зависимости между объектами и повышает гибкость, за счет возможности изменения реализаций.

Часто можно увидеть совместное использование Абстрактной фабрики с другими шаблонами:

Схожие шаблоны и их отличия

Абстрактная фабрика Фабричный метод Строитель
Порождает семейство объектов с определенными интерфейсами. Порождает один объект с определенным интерфейсом. Создает в несколько шагов один сложный (составной) объект.
Интерфейс, реализуемый классами. Метод класса, который переопределяется потомками. Интерфейс строителя, реализуемый классами, и класс для управления процессом.
Скрывает реализацию семейства объектов. Скрывает реализацию объекта. Скрывает процесс создания объекта, порождает требуемую реализацию.

Кроме того, Абстрактную фабрику можно рассматривать как набор Фабричных методов. Обратное утверждение не верно, поскольку произвольный набор не гарантирует создание объектов семейства.

Реализация шаблона в общем виде

  • разрабатываем интерфейсы объектов семейства и Абстрактной фабрики;
  • создаем семейства объектов и реализации Абстрактной фабрики для них;
  • в программе, например, в зависимости от версии ОС, конфигурации или другого параметра, порождается необходимая реализация Абстрактной фабрики.
  • в дальнейшем используется только интерфейсы как Абстрактной фабрики, так и порождаемых ей объектов.

Примеры реализации

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(), можно создавать только заданные реализации Абстрактной Фабрики.

Комментарии (18) -

Александр 06.10.2010 0:04:41

Спасибо за интересную статью, не могли бы вы в следующей описать разницу между паттернами Factory и Abstract Factory

Андрей 06.10.2010 14:06:48

Спасибо. Предвкушаю описание паттерна BuilderSmile

Спасибо за отзывы. Новые описания обязательно будут.

Bill Guest 08.10.2010 4:14:58

Восторг !
стал любить читать твой блог. Определенно жду продолжения патернофф Smile

А можно делать заказы на следующие стаьи по теме ?

@ Bill Guest: Спасибо. Насчет заказов - нет, но можете высказать пожелание.

Bill Guest 10.10.2010 1:36:16

мои пожеания - билдер и адаптер. а так же хотелось бы увидеть патерны для веба.

спацибо

Дмитрий 04.10.2011 15:05:15

Спасибо за замечательные описание шаблонов проектирования. Хотелось бы увидеть шаблон EventAggregator.

монголоид 20.04.2012 16:00:06

Отлично написано!

Алексей 22.10.2012 11:24:48

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

@ Алексей: Пример вызова - это метод GetUserInput(). В зависимости от переданного ему варианта фабрики, будет создано диалоговое окно соответствующего вида.

Андрей, большое спасибо за статью!

P.S. У Вас ссылка "параметризованный Фабричный метод." битая

@ Sergey: Пожалуйста. И вам спасибо за наблюдательность. Ссылку поправил.

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

@ B. Earlich: Пожалуйста.

Дмитрий 29.08.2014 17:23:11

Спасибо за статью, написано доступно, но мне как начинающему, некоторые вещи непонятны. Основные используемые паттерны на одном сайте, в книгах много воды льют. Отличный материал!!!!

Serhii Voznyi 28.08.2015 10:18:31

Шикарно описано, нашел блог случай, решил просто освежить память. Очень толково написано, кратко, по сути, без лишнего. Получил моральное и эстетическое удовольствие!  

Serhii Voznyi Спасибо.

Артем 27.09.2018 6:50:24

Спасибо за статью.
Но мне кажется тут есть над чем поразмыслить, а именно в отказе использования switch/case. Описанный подход выше нарушает один из важных принципов - Open/Close.

Добавить комментарий