Sunday, April 6, 2014

Использование паттерн Repository с BLToolkit. Часть третья

Пришло время и третьей статьи по паттерну репозиторий (Repository) с использованием компонентов BLToolkit. В этой части статьи я расскажу о тех вопросах, которые остались вне цикла первых статей, посвященных данному репозиторию. Мы рассмотрим вопрос конфигурирования BLToolkit для работы с двумя базами данных, а также как поступить в том случае, если некую часть логики нужно скрыть. Многие разработчики называют паттерн  репозиторий антипаттерном только из-за того, что он открывает всю реализацию, даже если нужно, чтобы некая часть логики не поддерживала все операции CRUD. Это не проблема, единственная проблема в данном аспекте может быть в недостатке знаний. 
Давайте на базе базового репозитория Repository<T>, который мы реализовали в предыдущей статье, расширим логику для таблицы AlcoPoduct. Суть нового репозитория будет заключаться в том, что у него будет только доступна операция выборки – Select. Для этого добавим новый интерфейс IAlcoProductEntity в проект UsingGenericRepository.Repository, который мы рассматриваем на протяжении двух статей.
Реализация данного интерфейса приведена ниже.
public interface IAlcoProductRepository
{
       IQueryable<AlcoProductEntity> GetAlcoProductByCode(int? code);

       IQueryable<AlcoProductEntity> GetAlcoProductByName(string name);

       IQueryable<AlcoProductEntity> FindAll(Expression<Func<AlcoProductEntity, bool>> where = null);
}
Теперь давайте реализуем поддержку данного интерфейса и создадим новый репозиторий. Добавим новый класс AlcoProductRepository, как показано на рисунке ниже.
Данный репозиторий будет иметь следующий вид:
public class AlcoProductRepository : Repository<AlcoProductEntity>, IAlcoProductRepository
{
       public IQueryable<AlcoProductEntity> GetAlcoProductByCode(int? code)
       {
             return Select(x => x.Code == code);
       }

       public IQueryable<AlcoProductEntity> GetAlcoProductByName(string name)
       {
             return Select(x => x.Name == name);
       }

       public IQueryable<AlcoProductEntity> FindAll(Expression<Func<AlcoProductEntity, bool>> @where = null)
       {
             return Select(@where);
       }
}
Многие разработчики твердят о дублировании кода, о том, что придётся реализовывать много одинаковой логики. Дублирования не будет, реализация не занимает много кода. Реализовать новый интерфейс, который будет базироваться на основании базового репозитория Repository<T>, проблем не составляет. Реализацию нового репозитория можно построить как в приведенном примере через наследование, так и через композицию. Выбирайте тот способ, который вам больше нравится, учитывая, что эти два способа в плане тестирования будут иметь свои локальные проблемы. Использование данного репозитория приведено ниже.
class Program
{
       static void Main(string[] args)
       {
             try
             {
                    var alcoRepository = new AlcoProductRepository();
                    var unit = alcoRepository.Select(x => x.Code == 2).First();
                    Console.WriteLine("EntityName = {0}", unit.Name);
             }
             catch (Exception ex)
             {
                    Console.WriteLine(ex.ToString());
             }
                   
             Console.ReadLine();
       }
}
Вывод результата на экран:
Если мы реализуем такой подход, как показан выше, то у нас будут доступны все методы с базового репозитория. Но для такого случая у нас есть вспомогательные средства, которые позволяют заменить жесткую привязку на более мягкую. Это делается с помощью внедрения зависимостей – Dependency Injection (DI). Чтобы более подробно ознакомиться, что такое DI, и какие проблемы позволяет решать данный подход, рекомендую посмотреть обзор популярных IoC контейнеров, которые используют подход внедрения зависимостей, если вы с этим никогда не сталкивались ранее.  
Для примера использую простой и легковесный IoC контейнер Autofac. Нам необходимо в проект добавить ссылку на данный IoC контейнер. Мы можем  это сделать с помощью Manage NuGet Packages, как показано на рисунке ниже.
Реализация с IoC контейнером Autofac будет выглядеть следующим образом.
class Program
{
       static void Main(string[] args)
       {
             try
             {
                    var builder = new ContainerBuilder();
                    builder.RegisterType<AlcoProductRepository>().As<IAlcoProductRepository>();
                    var container = builder.Build();
                    var alcoRepository = container.Resolve<IAlcoProductRepository>();

                    var unit = alcoRepository.GetAlcoProductByCode(2).First();
                    Console.WriteLine("EntityName = {0}", unit.Name);
             }
             catch (Exception ex)
             {
                    Console.WriteLine(ex.ToString());
             }
                   
             Console.ReadLine();
       }
}
Я предпочитаю использовать подход с помощью модульности, которая есть в данном IoC контейнере. Чтобы понять, как это сделать, вы можете посмотреть мою статью "Использование IoC контейнера Autofac". Тема принципов работы IoC контейнеров не является основной для данной статьи, поэтому продолжим наше рассмотрение паттерна репозиторий.

Настройка подключения к базе данных
Для того чтобы подключиться к базе данных, необходимо задать провайдер, который мы будем использовать. Это можно сделать как через файл конфигурации, так и через код. Список доступных провайдеров вы можете посмотреть здесь. Также по данной ссылке вы можете просмотреть примеры конфигурирования подключения к базе данных. Давайте рассмотрим пример настройки подключения к двум фейковым базам данных, данные для которых возьмем с примера выше.
Добавим файл конфигурации App.config:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add
            name             = "DemoConnectionOne"
            connectionString = "Server=.;Database=BLToolkitData;Integrated Security=SSPI"
            providerName     = "System.Data.SqlClient" />
    <add
            name             = "DemoConnectionSecond"
            connectionString = "Server=.;Database=BLToolkitDataSecond;Integrated Security=SSPI"
            providerName     = "System.Data.SqlClient" />
  </connectionStrings>
</configuration>
Теперь реализуем наши провайдеры.
/// <summary>
/// Менеджер запросов к СУБД
/// Инкапсулирует соединение и низкоуровневую логику работы с СУБД
/// </summary>
public class TestOneDataProvider<TP> : DbManager
       where TP : DataProviderBase, new()
{
       /// <summary>
       /// Ключ для строки соединения провайдера по умолчанию
       /// </summary>
       public const string DefaultConfigurationString = "DemoConnectionOne";

       #region [ .ctor ]

       /// <summary>
       /// Регистрируем ODP провайдер
       /// </summary>
       static TestOneDataProvider()
       {
             AddDataProvider(DefaultConfigurationString, new TP());
             AddConnectionString(DefaultConfigurationString, ConnectionString);
       }

       #endregion

       #region Constructor(s)

       public TestOneDataProvider()
             : this(DefaultConfigurationString)
       {
       }

       public TestOneDataProvider(string configurationString)
             : base(configurationString)
       {
       }

       #endregion

       public static string ConnectionString
       {
             get
             {
                    return ConfigurationManager.ConnectionStrings[DefaultConfigurationString].ConnectionString;
             }
       }
}

/// <summary>
/// Менеджер запросов к СУБД
/// Инкапсулирует соединение и низкоуровневую логику работы с СУБД
/// </summary>
public class TestSecondDataProvider<TP> : DbManager
       where TP : DataProviderBase, new()
{
       /// <summary>
       /// Ключ для строки соединения провайдера по умолчанию
       /// </summary>
       public const string DefaultConfigurationString = "DemoConnectionSecond";

       #region [ .ctor ]

       /// <summary>
       /// Регистрируем ODP провайдер
       /// </summary>
       static TestSecondDataProvider()
       {
             AddDataProvider(DefaultConfigurationString, new TP());
             AddConnectionString(DefaultConfigurationString, ConnectionString);
       }

       #endregion

       #region Constructor(s)

       public TestSecondDataProvider()
             : this(DefaultConfigurationString)
       {
       }

       public TestSecondDataProvider(string configurationString)
             : base(configurationString)
       {
       }

       #endregion

       public static string ConnectionString
       {
             get
             {
                    return ConfigurationManager.ConnectionStrings[DefaultConfigurationString].ConnectionString;
             }
       }
}
Они несложные, только подразумевают немного работы программиста. Теперь посмотрим, как это зарегистрировать через Autofac.
var builder = new ContainerBuilder();
//Установка для репозиториев AccessDataProvider
builder.Register<Func<Type, DbManager>>(c => e =>
{
       var fullName = e.FullName;

       if (string.IsNullOrEmpty(fullName))
             throw new NotSupportedException();

       if (fullName.Contains("DemoConnectionOne"))
             return new TestOneDataProvider<SqlDataProvider>();

       if (fullName.Contains("DemoConnectionSecond"))
             return new TestSecondDataProvider<AccessDataProvider>();

       throw new NotSupportedException();
}).AsSelf();

builder.RegisterGeneric(typeof(Repository<>))
             .PropertiesAutowired()
             .AsSelf();

builder.RegisterType<AlcoProductRepository>().As<IAlcoProductRepository>();
var container = builder.Build();
Основная проблема состоит в том, что нужно знать, как связать это все, через какой-то IoC контейнер. Тут нужно больше знаний самого IoC контейнера и как это сделать через него, чем знание компонента BLToolkit.

Создание одного репозитория для нескольких моделей

Следующей надуманной проблемой некоторых разработчиков становится то, что, по их мнению, для того чтобы использовать репозиторий, сложно использовать на несколько моделей сразу. Пример такой проблемы можно посмотреть в статье "If you're going to use repositories, don't have generic or base repositories...". Для компонента BLToolkit это не является проблемой. Задача разработчика будет заключаться только в создании кастомного класса, наследуемого от DataAccessor. В официальной документации есть замечательный пример, который показывает, как это реализовать. Думаю, у разработчика, который дочитал до этого момента, должно хватить знаний, чтобы это реализовать на уровне паттерна Repository. Самое сложное при использовании generic repository – это заставить себя не злоупотреблять базовым шаблоном. Не то выйдет у вас в коде абстракция на абстракции и будет абстракцией погонять. Надеюсь, что статья получилась не очень запутанной и после ее прочтения вы сможете использовать паттерн репозиторий в своих проектах. В одной из следующих статей рассмотрим реализацию данного паттерна для Entity Framework

No comments:

Post a Comment