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

Petro Bazeliuk —  Март 24, 2016 — Оставьте комментарий

 

Совсем недавно, обсуждая RabbitMQ и 1С:Предприятие, попросили написать прокси-сервер\службу для вызова сервера 1С:Предприятие. Но для того, что бы статья не была слишком большой хочу рассмотреть паттерн проектирования отдельно. Вкратце будет рассмотрен сам паттерн и такие понятия как: Inversion of Control, IoC-container и Dependency Injection.  

 

Abstract factory (абстрактная фабрика) — предоставляет интерфейс для создания семейств, связанных между собой, или независимых объектов, конкретные классы которых неизвестны (Gang of Four).
Допустим у нас есть несколько источников данных — файл XML, база данных SQL и Fake объект. В этих источниках данных хранятся данные об приоритетах задач и типах задач. Соответственно источник данных приоритетов задач — AbstractProductA, а источник типов задач — AbstractProductB. Конкретные продукты это реализации источников данных (XML, SQL и Fake). Итого имеем:

  • XmlRepositoryFactory с продуктами XmlPriorityRepository (приоритеты задач из XML) и XmlTaskTypeRepository (типы задачи из XML);
  • SqlRepositoryFactory с продуктами SqlPriorityRepository (приоритеты задачи из SQL) и SqlTaskTypeRepository (типы задачи из SQL);
  • FakeRepositoryFactory с продуктами FakePriorityRepository (приоритеты задачи из Fake) и FakeTaskTypeRepository (типы задачи из Fake).

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

// Класс реализация приоритетов задач
public class Priority
{
    public int PriorityId { get; set; }
}

// Класс реализация типов задач
public class TaskType
{
    public int TaskTypeId { get; set; }
}

Теперь опишем интерфейсы продуктов и конкретные реализации:

// Интерфейс источника приоритетов задач
public interface IPriorityRepository
{
    List GetAllPriorities();
}

// Реализация XML источника приоритетов
class XmlPriorityRepository : IPriorityRepository
{

    #region Fileds

    private readonly string _connectionString;

    #endregion

    #region Constructors

    public XmlPriorityRepository(string connectionString)
    {
        this._connectionString = connectionString;
    }

    #endregion

    public List GetAllPriorities()
    {
        using (XmlReader reader = XmlReader.Create(_connectionString))
        {
            List listOfPriorities = new List();
            while (reader.Read())
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        break;
                    case XmlNodeType.Text:
                        Priority myTask = new Priority();
                        myTask.PriorityId = Convert.ToInt32(reader.Value);
                        listOfPriorities.Add(myTask);
                        break;
                    case XmlNodeType.XmlDeclaration:
                    case XmlNodeType.ProcessingInstruction:
                        break;
                    case XmlNodeType.Comment:
                        break;
                    case XmlNodeType.EndElement:
                        break;
                }
            }
            return listOfPriorities;
        }
    }
}

// Реализация SQL источника приоритетов
class SqlPriorityRepository : IPriorityRepository
{

    #region Queries

    private const string SELECT_SECTIONS = "SELECT [Id] FROM [dbo].[tblPriorities]";

    #endregion

    #region Fileds

    private readonly string _connectionString;

    #endregion

    #region Constructors

    public SqlPriorityRepository(string connectionString)
    {
        this._connectionString = connectionString;
    }

    #endregion

    public List GetAllPriorities()
    {
        using (var connection = new SqlConnection(this._connectionString))
        {
            connection.Open();
            using (var command = new SqlCommand())
            {
                command.Connection = connection;
                command.CommandText = SELECT_SECTIONS;
                command.CommandType = System.Data.CommandType.Text;

                using (var reader = command.ExecuteReader())
                {
                    List listOfPriorities = new List();
                    while (reader.Read())
                    {
                        Priority myTask = new Priority();
                        myTask.PriorityId = (int)reader["Id"];
                        listOfPriorities.Add(myTask);
                    }
                    return listOfPriorities;
                }
            }
        }
    }
}

// Реализация Fake источника приоритетов
class FakePriorityRepository : IPriorityRepository
{
    public List GetAllPriorities()
    {
        List listOfPriorities = new List();
        listOfPriorities.Add(new Priority() { PriorityId = 10 });
        listOfPriorities.Add(new Priority() { PriorityId = 20 });
        listOfPriorities.Add(new Priority() { PriorityId = 30 });
        listOfPriorities.Add(new Priority() { PriorityId = 40 });

        return listOfPriorities;
    }
}
// Интерфейс источника типов задач
public interface ITaskTypeRepository
{
    List GetAllTaskTypes();
}

// Реализация XML источника типов задач
class XmlTaskTypeRepository : ITaskTypeRepository
{

    #region Fileds

    private readonly string _connectionString;

    #endregion

    #region Constructors

    public XmlTaskTypeRepository(string connectionString)
    {
        this._connectionString = connectionString;
    }

    #endregion

    public List GetAllTaskTypes()
    {
        using (XmlReader reader = XmlReader.Create(_connectionString))
        {
            List listOfTasks = new List();
            while (reader.Read())
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        break;
                    case XmlNodeType.Text:
                        TaskType myTaskType = new TaskType();
                        myTaskType.TaskTypeId = Convert.ToInt32(reader.Value);
                        listOfTasks.Add(myTaskType);
                        break;
                    case XmlNodeType.XmlDeclaration:
                    case XmlNodeType.ProcessingInstruction:
                        break;
                    case XmlNodeType.Comment:
                        break;
                    case XmlNodeType.EndElement:
                        break;
                }
            }
            return listOfTasks;
        }
    }
}

// Реализация SQL источника типов задач
class SqlTaskTypeRepository : ITaskTypeRepository
{

    #region Queries

    private const string SELECT_SECTIONS = "SELECT [Id] FROM [dbo].[tblTaskTypes]";

    #endregion

    #region Fileds

    private readonly string _connectionString;

    #endregion

    #region Constructors

    public SqlTaskTypeRepository(string connectionString)
    {
        this._connectionString = connectionString;
    }

    #endregion

    public List GetAllTaskTypes()
    {
        using (var connection = new SqlConnection(this._connectionString))
        {
            connection.Open();
            using (var command = new SqlCommand())
            {
                command.Connection = connection;
                command.CommandText = SELECT_SECTIONS;
                command.CommandType = System.Data.CommandType.Text;

                using (var reader = command.ExecuteReader())
                {
                    List listOfTasks = new List();
                    while (reader.Read())
                    {
                        TaskType myTaskType = new TaskType();
                        myTaskType.TaskTypeId = (int)reader["Id"];
                        listOfTasks.Add(myTaskType);
                    }
                    return listOfTasks;
                }
            }
        }
    }
}

// Реализация Fake источника типов задач
class FakeTaskTypeRepository : ITaskTypeRepository
{
    public List GetAllTaskTypes()
    {
        List listOfTasks = new List();
        listOfTasks.Add(new TaskType() { TaskTypeId = 1 });
        listOfTasks.Add(new TaskType() { TaskTypeId = 2 });
        listOfTasks.Add(new TaskType() { TaskTypeId = 3 });
        listOfTasks.Add(new TaskType() { TaskTypeId = 4 });

        return listOfTasks;
    }
}

Приступим к интерфейсу абстрактной фабрики и конкретных реализаций:

// Интерфейс абстрактной фабрики
public interface IRepositoryFactory
{
    ITaskTypeRepository CreateTaskTypeRepository();
    IPriorityRepository CreatePriorityRepository();
}

// Реализация конкретной фабрики XML
class XmlRepositoryFactory : IRepositoryFactory
{
    private readonly string _connectionString = @"db.xml";

    public IPriorityRepository CreatePriorityRepository()
    {
        IPriorityRepository PriorityRepository = new XmlPriorityRepository(_connectionString);
        return PriorityRepository;
    }

    public ITaskTypeRepository CreateTaskTypeRepository()
    {
        ITaskTypeRepository TaskTypeRepository = new XmlTaskTypeRepository(_connectionString);
        return TaskTypeRepository;
    }
}

// Реализация конкретной фабрики SQL 
class SqlRepositoryFactory : IRepositoryFactory
{
    private readonly string _connectionString = @"Server = localhost; user = sa; password = sa; Database = db;";

    public IPriorityRepository CreatePriorityRepository()
    {
        IPriorityRepository PriorityRepository = new SqlPriorityRepository(_connectionString);
        return PriorityRepository;
    }

    public ITaskTypeRepository CreateTaskTypeRepository()
    {
        ITaskTypeRepository TaskTypeRepository = new SqlTaskTypeRepository(_connectionString);
        return TaskTypeRepository;
    }
}

// Реализация конкретной фабрики Fake
class FakeRepositoryFactory : IRepositoryFactory
{
    public IPriorityRepository CreatePriorityRepository()
    {
        IPriorityRepository PriorityRepository = new FakePriorityRepository();
        return PriorityRepository;
    }

    public ITaskTypeRepository CreateTaskTypeRepository()
    {
        ITaskTypeRepository TaskTypeRepository = new FakeTaskTypeRepository();
        return TaskTypeRepository;
    }
}

Простой пример использования: использование на выбор источника данных для получения информации:

static void Main(string[] args)
{
    IRepositoryFactory Factory;

    Console.WriteLine("Enter repository:\n 1. SQL \n 2. XML \n 3. FAKE");
    string s = Console.ReadLine();
    int result = Convert.ToInt32(s);
    if (result == 1)
    {
        Factory = new SqlRepositoryFactory();
    }
    else if (result == 2)
    {
        Factory = new XmlRepositoryFactory();
    }
    else if (result == 3)
    {
        Factory = new FakeRepositoryFactory();
    }
    else
    {
        return;
    }

    ITaskTypeRepository TaskTypeRepository = Factory.CreateTaskTypeRepository();
    List taskTypes = TaskTypeRepository.GetAllTaskTypes();
    foreach (var item in taskTypes)
    {
        Console.WriteLine("Task ID: {0:d}", item.TaskTypeId);
    }

    IPriorityRepository PriorityRepository = Factory.CreatePriorityRepository();
    List priorities = PriorityRepository.GetAllPriorities();
    foreach (var item in priorities)
    {
        Console.WriteLine("Priority ID: {0:d}", item.PriorityId);
    }
}

Абстрактная фабрика широко используемый дизайн паттерн. Отличным примером является ADO.NET DbProviderFactory, которая есть абстрактной фабрикой, что определяет интерфейсы для получения DbCommand, DbConnection, DbParameter и т. п. Конкретная фабрика SqlClientFactory возвратит соответственно SqlCommand, SqlConnection и т.п. Это позволяет работать с разными источниками данных.

Паттерн обладает следующими плюсами и минусами (источник: Gang of Four):
+ изолирует конкретные классы. Помогает контролировать классы объектов, создаваемых приложением. Поскольку фабрика инкапсулирует ответственность за создание классов и сам процесс их создания, то она изолирует клиента от деталей реализации классов. Клиенты манипулируют экземплярами через их абстрактные интерфейсы. Имена изготавливаемых классов известны только конкретной фабрике, в коде клиента они не упоминаются;
+ упрощает замену семейств продуктов. Класс конкретной фабрики появляется в приложении только один раз: при инстанцировании. Это облегчает замену используемой приложением конкретной фабрики. Приложение может изменить конфигурацию продуктов, просто подставив новую конкретную фабрику. Поскольку абстрактная фабрика создает все семейство продуктов, то и заменяется сразу все семейство;
+ гарантирует сочетаемость продуктов. Если продукты некоторого семейства спроектированы для совместного использования, то важно, чтобы приложение в каждый момент времени работало только с продуктами единственного семейства. Абстрактная фабрика позволяет легко соблюсти это ограничение;
 поддержать новый вид продуктов трудно. Расширение абстрактной фабрики для изготовления новых видов продуктов — непростая задача. Интерфейс абстрактная фабрика фиксирует набор продуктов, которые можно создать. Для поддержки новых продуктов необходимо расширить интерфейс фабрики, то есть изменить класс абстрактная фабрика и все его подклассы.

Внедрение зависимостей (Dependency Injection)

Dependency Injection — процесс предоставления внешней зависимости программному компоненту. Является специфичной формой «инверсии управления» (англ. Inversion of control, IoC), когда она применяется к управлению зависимостями. В полном соответствии с принципом единой обязанности объект отдаёт заботу о построении требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму.
IoC-контейнер — фреймворк, обеспечивающий внедрение зависимостей. Очень мне нравится это описание — это компоновщик, который собирает не объектные файлы, а объекты ООП (экземпляры класса) во время исполнения программы. Условно, если объекту нужно получить доступ к определенному сервису, объект берет на себя ответственность за доступ к этому сервису: он или получает прямую ссылку на местонахождение сервиса, или обращается к известному «сервис-локатору» и запрашивает ссылку на реализацию определенного типа сервиса. Используя же внедрение зависимости, объект просто предоставляет свойство, которое в состоянии хранить ссылку на нужный тип сервиса; и когда объект создается, ссылка на реализацию нужного типа сервиса автоматически вставляется в это свойство (поле), используя средства среды.

Абстрактная фабрика зачастую передается через конструктор и по определению реализуется через интерфейс или абстрактный класс. Соответственно один из используемых паттернов DI — constructor injection. Суть состоит в том, что все зависимости, требуемые некоторому классу передаются ему в качестве параметров конструктора, представленных в виде интерфейсов или абстрактных классов. Такая зависимость используется в прокси-сервере\службе для вызова сервера 1С:Предприятие, но в данном статье, в качестве примера, используется «сервис-локатор» и IoC-контейнер Unity (хорошая книга).

Перейдем к реализации с использованием IoC-контейнера. Первое что нужно это установить IoC-контейнер Unity из NuGet консоли командой: install-package Unity. Далее, необходимо настроить UnityConfig.cs. Создадим IoC-контейнер, устанавливаем контейнер как «сервис-локатор» через SetLocatorProvider, получаем строку соединения с базой данных из конфигурационного файла и регистрируем пары интерфейсы-типы (все это можно сделать и в конфигурационном файле, в данном примере на лету переключения между источниками данных не будет).

public static class UnityConfig
{
    public static void Initialize()
    {
        IUnityContainer myContainer = new UnityContainer();

        ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(myContainer));

        string connectionString = ConfigurationManager.ConnectionStrings["TestDB"].ConnectionString;

        myContainer.RegisterType<ITaskTypeRepository, SqlTaskTypeRepository>(new InjectionConstructor(connectionString));
        myContainer.RegisterType<IPriorityRepository, SqlPriorityRepository>(new InjectionConstructor(connectionString));

    }
}

Пример использования:

static void Main(string[] args)
{

    UnityConfig.Initialize();
         
    ITaskTypeRepository TaskTypeRepository = ServiceLocator.Current.GetInstance();
    List taskTypes = TaskTypeRepository.GetAllTaskTypes();
    foreach (var item in taskTypes)
    {
        Console.WriteLine("Task ID: {0:d}", item.TaskId);
    }


    IPriorityRepository PriorityRepository = ServiceLocator.Current.GetInstance();
    List priorities = PriorityRepository.GetAllPriorities();
    foreach (var item in priorities)
    {
        Console.WriteLine("Priority ID: {0:d}", item.PriorityId * 10);
    }

}

В этом примере, IoC-контейнер взял на себя создание необходимых классов, регистрацию которых можно с легкостью перенести в конфигурационный файл, что бы так же легко переключать источники данных. Статья не предназначена указать, что нужно использовать абстрактную фабрику или внедрение зависимостей всегда и везде — это что было использовано, при написании прокси-сервера\службы для вызова сервера 1С:Предприятие с некоторыми изменениями. Для построения полной картины, для тех кто не в теме, прочтите несколько фундаментальных книг о паттернах.

P.S. Надеюсь, все поняли какие классы оказались лишними во втором примере, после первого примера 🙂

Petro Bazeliuk

Записи

Опыт работы с «1С:Предприятие 8» — более 10 лет, за это время реализовано 30 успешных проектов по итеративным методологиям Scrum и Kanban. Оптимальные решения для высоконагруженных ИБ с онлайном от 400 человек. Занимаюсь продвижением в массы системы контроля версий — git и методики git-flow, TDD, BDD, а также проработкой паттерна минимальной модификации конфигурации и внесением изменений без обновления базы данных. Время от времени участвую в проекте xUnitFor1C.

Комментариев нет

Be the first to start the conversation!

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

w

Connecting to %s