Совсем недавно, обсуждая 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. Надеюсь, все поняли какие классы оказались лишними во втором примере, после первого примера 🙂