29 октября 2008 г.

TDD и LLBLGen Pro

LLBLGen Pro - это O/R mapper, который генерирует .NET код, основываясь на схеме в базе данных.

Сейчас мы активно его используем для работы над нашим проектом. Надо отметить, что эта ORM имеет очень хороший интерфейс и настройку.

К тому же, они сделали поддержку запросов к базе данных через Linq. В LLBLGen вся работа происходит с помощью класса LinqMetaData.

К примеру, нужно найти все объекты RealtyEntity в базе:

   1:  using (var adapter = new DataAccessAdapter())
   2:  {
   3:      var linqMetaData = new LinqMetaData(adapter);
   4:      result = linqMetaData.Realty.ToList();
   5:  }

Отлично! Но здесь есть одна проблема. Весь код сгенерированный LLBLGen жестко завязан на типах используемых в этой ORM.

Заметили, как идёт обращение к IQueryable объекту для сущности RealtyEntity:

   1:  linqMetaData.Realty...

В сгенерированном классе LinqMetaData это выглядит примерно так:

   1:  public DataSource2<realtyentity> Realty
   2:  {
   3:      get { return new DataSource2<realtyentity>(_adapterToUse, new ElementCreator(), _customFunctionMappings, _contextToUse); }
   4:  }

Т.е. для каждой сущности генерируется свой getter, который и формирует DataSource2 (это расширенный IQueryable) объект для конкретного класса.

Проблемы, которые это влечет:

  • сложно перейти на L2Q, EF и любую ORM, которая поддерживает дженерик объекты IQueryable.
  • для тестирования кода, который обращается к базе данных нужно обязательно писать интеграционные тесты. Т.е. для того, чтобы проверить логику работы моего объекта RealtyRepository, мне нужно ждать, пока фиктивная база данных инициализирутся, заполнится нужными значениями и на ней пройдет тест. При хорошем темпе работы это отнимает слишком много времени.

Почему не сделать у объекта LinqMetaData дженерик поле IQueryable? Иначе мне нужно указывать название этого поля вместо типа сущности.

Что в итоге хочется получить:

   1:  using (var adapter = new DataAccessAdapter())
   2:  {
   3:      var linqMetaData = new LinqMetaData(adapter);
   4:      result = linqMetaData.Query<realtyentity>().ToList();
   5:  }

Именно так делается в Linq to SQL, Entity Framework. Даже в NHibernate Linq сделали подобным образом. К тому же, если есть возможность абстрагироваться от ORM, то надо это сделать.

Нигде не мог найти, как же LLBLGen Pro заботиться о TDD. Вот мой совет, как это сделать.

Проблема

Мы хотим использовать код типа:

   1:  linqMetaData.Query<realtyentity>().ToList();

При этом иметь возможность "подставлять" базу данных в памяти (in-memory DB) для быстрого запуска модульных тестов.

Решение

Нужно заменять объект LinqMetaData подобным mock-объектом. К примеру, использовать фабрику объектов LinqMetaData и в ней подставлять нужную реализацию LinqMetaData в зависимости от контекста. Т.е. LinqMetaDataFactory будет возвращать LinqMetaData при использовании приложения и MockLinqMetaData при модульном тестировании.

   1:  ??? linqMetaData = LinqMetaDataFactory.Create(adapter);

Нам нужен общий интерфейс или абстрактрый класс, который будет возвращаться фабрикой LinqMetaDataFactory. Но класс LinqMetaData не имеет интерфейса и постоянно перегенерируется, его трогать нельзя.

Здесь очень пригодится паттерн Adapter.

По нашей задумке интерфейс, который возвращается фабрикой, должен выглядеть так:

   1:  public interface ILinqMetaDataAdapter
   2:  {
   3:      IQueryable<t> Query<t>() where T : IEntityCore;
   4:  }

Осталось реализовать объект адаптера, который обернет LinqMetaData:

   1:  public class LinqMetaDataAdapter : ILinqMetaDataAdapter
   2:  {
   3:      private readonly Dictionary<type, func=""><iqueryable>> actions;
   4:      private readonly LinqMetaData innerLinq;
   5:   
   6:      public LinqMetaDataAdapter(IDataAccessAdapter adapter)
   7:      {
   8:          innerLinq = new LinqMetaData(adapter);
   9:   
  10:      // ставим в соответствие тип и DataSource2 объект для этого типа в сгенерированном классе LinqMetaData
  11:      actions = new Dictionary<type, func=""><iqueryable>>
  12:              {
  13:                  {typeof (AddressCityEntity), () => innerLinq.AddressCity},
  14:                  {typeof (SiteSectionEntity), () => innerLinq.SiteSection}
  15:              };
  16:      }
  17:   
  18:      #region ILinqMetaDataAdapter Members
  19:   
  20:      public IQueryable<t> Query<t>() where T : IEntityCore
  21:      {
  22:          return (IQueryable<t>) actions[typeof (T)].Invoke();
  23:      }
  24:   
  25:      #endregion
  26:      }
  27:  }

Теперь ничего не мешает нам использовать Unity, StructureMap или другой фремворк, для подстановки нашей реализации в фабрику:

   1:  public static class LinqMetaDataFactory
   2:  {
   3:      public static ILinqMetaDataAdapter Create(IDataAccessAdapter adapter)
   4:      {
   5:          return SomeIoCContainer.Resolve<ilinqmetadataadapter>(adapter);
   6:      }
   7:  }

Конечный результат:

   1:  using (var adapter = new DataAccessAdapter())
   2:  {
   3:      ILinqMetaDataAdapter linqMetaData = LinqMetaDataFactory.Create(adapter);
   4:      result = linqMetaData.Query<realtyentity>().ToList();
   5:  }

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

Отправить комментарий