Луковая архитектура, единица работы и общий шаблон репозитория

Это первый раз, когда я применяю подход к проектированию, ориентированный на предметную область. Я решил попробовать Onion Architecture, поскольку она ориентирована на домен а не в инфраструктуре / платформах / и т. д.

введите описание изображения здесь

Чтобы абстрагироваться от Entity Framework, я создал общий репозиторий с реализацией Единицы работы.

Интерфейсы IRepository<T> и IUnitOfWork:

public interface IRepository<T>
{
    void Add(T item);

    void Remove(T item);

    IQueryable<T> Query();
}

public interface IUnitOfWork : IDisposable
{
    void SaveChanges();
}

Реализации Entity Framework IRepository<T> и IUnitOfWork:

public class EntityFrameworkRepository<T> : IRepository<T> where T : class
{
    private readonly DbSet<T> dbSet;

    public EntityFrameworkRepository(IUnitOfWork unitOfWork)
    {
        var entityFrameworkUnitOfWork = unitOfWork as EntityFrameworkUnitOfWork;

        if (entityFrameworkUnitOfWork == null)
        {
            throw new ArgumentOutOfRangeException("Must be of type EntityFrameworkUnitOfWork");
        }

        dbSet = entityFrameworkUnitOfWork.GetDbSet<T>();
    }

    public void Add(T item)
    {
        dbSet.Add(item);
    }

    public void Remove(T item)
    {
        dbSet.Remove(item);
    }

    public IQueryable<T> Query()
    {
        return dbSet;
    }
}

public class EntityFrameworkUnitOfWork : IUnitOfWork
{
    private readonly DbContext context;

    public EntityFrameworkUnitOfWork()
    {
        this.context = new CustomerContext();;
    }

    internal DbSet<T> GetDbSet<T>()
        where T : class
    {
        return context.Set<T>();
    }

    public void SaveChanges()
    {
        context.SaveChanges();
    }

    public void Dispose()
    {
        context.Dispose();
    }
}

Репозиторий Клиент:

public interface ICustomerRepository : IRepository<Customer>
{

}

public class CustomerRepository : EntityFrameworkRepository<Customer>, ICustomerRepository 
{
    public CustomerRepository(IUnitOfWork unitOfWork): base(unitOfWork)
    {
    }
}

Контроллер ASP.NET MVC с использованием репозитория:

public class CustomerController : Controller
{
    UnityContainer container = new UnityContainer();

    public ActionResult List()
    {
        var unitOfWork = container.Resolve<IUnitOfWork>();
        var customerRepository = container.Resolve<ICustomerRepository>();

        return View(customerRepository.Query());
    }

    [HttpPost]
    public ActionResult Create(Customer customer)
    {
        var unitOfWork = container.Resolve<IUnitOfWork>();
        var customerRepository = container.Resolve<ICustomerRepository>();; 

        customerRepository.Add(customer);

        unitOfWork.SaveChanges();

        return RedirectToAction("List");
    }
}

Внедрение зависимости с единством:

container.RegisterType<IUnitOfWork, EntityFrameworkUnitOfWork>();
container.RegisterType<ICustomerRepository, CustomerRepository>();

Решение:

введите описание изображения здесь

ПРОБЛЕМЫ?

  • Реализация репозитория (код EF) очень универсальна. Все это находится в стороне от класса EntityFrameworkRepository<T>. Репозитории конкретных моделей не содержат такой логики. Это избавляет меня от написания тонны избыточного кода, но, возможно, приносит в жертву гибкость?

  • Классы ICustomerRepository и CustomerRepository в основном пусты. Они созданы исключительно для того, чтобы обеспечить абстракцию. Насколько я понимаю, это соответствует видению луковой архитектуры, где инфраструктура и платформенно-зависимый код находятся вне вашей системы, но иметь пустые классы и пустые интерфейсы кажется неправильным?

  • Чтобы использовать другую реализацию сохранения (например, хранилище таблиц Azure), необходимо создать новый класс CustomerRepository, который унаследует AzureTableStorageRepository<T>. Но это может привести к избыточному коду (несколько репозиториев клиентов)? Как бы этот эффект издевательства?

  • Другая реализация (например, хранилище таблиц Azure) имеет ограничения на международную поддержку, поэтому класс AzureTableStorageUnitOfWork не будет работать в этом контексте.

Есть ли другие проблемы с тем, как я это сделал?

(В основном я черпал вдохновение из этого сообщения)


person Dave New    schedule 09.12.2013    source источник
comment
Вы используете анти-шаблон локатора служб, взяв зависимость на контейнере IoC. Вместо этого вы должны зарегистрировать фабричный класс для внедрения в ваш контроллер. Некоторые контейнеры IoC могут создавать для вас один из них в виде Func<T> зависимости.   -  person AlexFoxGill    schedule 10.12.2013


Ответы (3)


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

Давайте пройдемся по некоторым из них.

1. Внедрение зависимостей (DI) и использование IoC.

Вы используете простейшую версию шаблона Service Locator - container сам экземпляр .

Я предлагаю вам использовать «инъекцию конструктора». Дополнительную информацию можно найти здесь (ASP .NET MVC 4 Dependency Injection).

public class CustomerController : Controller
{
    private readonly IUnitOfWork unitOfWork;
    private readonly ICustomerRepository customerRepository;

    public CustomerController(
        IUnitOfWork unitOfWork, 
        ICustomerRepository customerRepository)
    {
        this.unitOfWork = unitOfWork;
        this.customerRepository = customerRepository;
    }

    public ActionResult List()
    {
        return View(customerRepository.Query());
    }

    [HttpPost]
    public ActionResult Create(Customer customer)
    {
        customerRepository.Add(customer);
        unitOfWork.SaveChanges();
        return RedirectToAction("List");
    }
}

2. Объем единицы работы (UoW).

Я не могу найти образ жизни IUnitOfWork и ICustomerRepository. Я не знаком с Unity, но msdn говорит, что TransientLifetimeManager используется по умолчанию. Это означает, что вы будете получать новый экземпляр каждый раз, когда разрешаете type.

Итак, следующий тест не проходит:

[Test]
public void MyTest()
{
    var target = new UnityContainer();
    target.RegisterType<IUnitOfWork, EntityFrameworkUnitOfWork>();
    target.RegisterType<ICustomerRepository, CustomerRepository>();

    //act
    var unitOfWork1 = target.Resolve<IUnitOfWork>();
    var unitOfWork2 = target.Resolve<IUnitOfWork>();

    // assert
    // This Assert fails!
    unitOfWork1.Should().Be(unitOfWork2);
} 

И я ожидаю, что экземпляр UnitOfWork в вашем контроллере отличается от экземпляра UnitOfWork в вашем репозитории. Иногда это может привести к ошибкам. Но это не выделено в ASP. .NET MVC 4 Dependency Injection как проблема для Unity.

В Замке Виндзор PerWebRequest стиль жизни используется для совместного использования одного и того же экземпляра типа в одном HTTP-запросе.

Это распространенный подход, когда UnitOfWork является компонентом PerWebRequest. Пользовательский ActionFilter может использоваться для вызова Commit() во время вызова метода OnActionExecuted().

Я бы также переименовал метод SaveChanges() и назвал его просто Commit, как он вызывается в примере < / a> и в PoEAA.

public interface IUnitOfWork : IDisposable
{
    void Commit();
}

3.1. Зависимости от репозиториев.

Если ваши репозитории будут «пустыми», создавать для них определенные интерфейсы не нужно. Можно разрешить IRepository<Customer> и иметь следующий код в вашем контроллере

public CustomerController(
    IUnitOfWork unitOfWork, 
    IRepository<Customer> customerRepository)
{
    this.unitOfWork = unitOfWork;
    this.customerRepository = customerRepository;
}

Есть тест, который это проверяет.

[Test]
public void MyTest()
{
    var target = new UnityContainer();
    target.RegisterType<IRepository<Customer>, CustomerRepository>();

    //act
    var repository = target.Resolve<IRepository<Customer>>();

    // assert
    repository.Should().NotBeNull();
    repository.Should().BeOfType<CustomerRepository>();
}

Но если вы хотите иметь репозитории, которые представляют собой «слой абстракции над слоем сопоставления, где сконцентрирован код построения запроса». (PoEAA, репозиторий)

Репозиторий является посредником между уровнями отображения домена и данных, действуя как коллекция объектов домена в памяти. Клиентские объекты декларативно составляют спецификации запросов и отправляют их в репозиторий для удовлетворения.

3.2. Наследование EntityFrameworkRepository.

В этом случае я бы создал простой IRepository

public interface IRepository
{
    void Add(object item);

    void Remove(object item);

    IQueryable<T> Query<T>() where T : class;
}

и его реализация, которая умеет работать с инфраструктурой EntityFramework и может быть легко заменена другой (например, AzureTableStorageRepository).

public class EntityFrameworkRepository : IRepository
{
    public readonly EntityFrameworkUnitOfWork unitOfWork;

    public EntityFrameworkRepository(IUnitOfWork unitOfWork)
    {
        var entityFrameworkUnitOfWork = unitOfWork as EntityFrameworkUnitOfWork;

        if (entityFrameworkUnitOfWork == null)
        {
            throw new ArgumentOutOfRangeException("Must be of type EntityFrameworkUnitOfWork");
        }

        this.unitOfWork = entityFrameworkUnitOfWork;
    }

    public void Add(object item)
    {
        unitOfWork.GetDbSet(item.GetType()).Add(item);
    }

    public void Remove(object item)
    {
        unitOfWork.GetDbSet(item.GetType()).Remove(item);
    }

    public IQueryable<T> Query<T>() where T : class
    {
        return unitOfWork.GetDbSet<T>();
    }
}

public interface IUnitOfWork : IDisposable
{
    void Commit();
}

public class EntityFrameworkUnitOfWork : IUnitOfWork
{
    private readonly DbContext context;

    public EntityFrameworkUnitOfWork()
    {
        this.context = new CustomerContext();
    }

    internal DbSet<T> GetDbSet<T>()
        where T : class
    {
        return context.Set<T>();
    }

    internal DbSet GetDbSet(Type type)
    {
        return context.Set(type);
    }

    public void Commit()
    {
        context.SaveChanges();
    }

    public void Dispose()
    {
        context.Dispose();
    }
}

И теперь CustomerRepository может быть прокси и ссылаться на него.

public interface IRepository<T> where T : class
{
    void Add(T item);

    void Remove(T item);
}

public abstract class RepositoryBase<T> : IRepository<T> where T : class
{
    protected readonly IRepository Repository;

    protected RepositoryBase(IRepository repository)
    {
        Repository = repository;
    }

    public void Add(T item)
    {
        Repository.Add(item);
    }

    public void Remove(T item)
    {
        Repository.Remove(item);
    }
}

public interface ICustomerRepository : IRepository<Customer>
{
    IList<Customer> All();

    IList<Customer> FindByCriteria(Func<Customer, bool> criteria);
}

public class CustomerRepository : RepositoryBase<Customer>, ICustomerRepository
{
    public CustomerRepository(IRepository repository)
        : base(repository)
    { }

    public IList<Customer> All()
    {
        return Repository.Query<Customer>().ToList();
    }

    public IList<Customer> FindByCriteria(Func<Customer, bool> criteria)
    {
        return Repository.Query<Customer>().Where(criteria).ToList();
    }
}
person Ilya Palkin    schedule 10.12.2013
comment
Спасибо за отличный ответ. (1) Готово (2) Я боролся с этой проблемой. Теперь, используя PerResolveLifetimeManager (3.1), я использую специальные интерфейсы для определенных методов выборки (например, GetCustomerByName). Я решил удалить метод IQueryable ‹T› Query () из репозиториев, поскольку он передавал данные EF в клиентский код. (3.2) Я рассмотрю это поближе. - person Dave New; 11.12.2013

Единственный недостаток, который я вижу, заключается в том, что вы сильно зависите от своего инструмента IOC, поэтому убедитесь, что ваша реализация надежна. Однако это характерно не только для луковых дизайнов. Я использовал Onion в ряде проектов и не сталкивался ни с какими реальными подводными камнями.

person Maess    schedule 09.12.2013
comment
Разве IoC не имеет решающего значения для работы Onion? Я мог бы объявить конкретные классы в вызывающем коде, но тогда я значительно увеличу связь и уменьшу тестируемость? Спасибо за ответ :) - person Dave New; 09.12.2013
comment
Да, поэтому я говорю, что его реализация должна быть надежной. Вам нужно использовать DI и IOC, чтобы Onion работал. - person Maess; 09.12.2013

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

Первая проблема - это взаимосвязь между репозиториями и UoW.

    var unitOfWork = container.Resolve<IUnitOfWork>();
    var customerRepository = container.Resolve<ICustomerRepository>();

Вот неявная зависимость. Репозиторий не будет работать без UoW! Не все репозитории нужно подключать к UoW. Например, как насчет хранимых процедур? У вас есть хранимая процедура и вы прячете ее за репозиторием. Вызов хранимой процедуры использует отдельную транзакцию! По крайней мере, не во всех случаях. Поэтому, если я разрешу единственный репозиторий и добавлю элемент, он не будет работать. Более того, этот код не будет работать, если я установлю Transient life license, потому что в репозитории будет другой экземпляр UoW. Итак, у нас есть сильная неявная связь.

Вторая проблема: вы создаете тесную связь между движком контейнера DI и используете его в качестве локатора сервисов! Локатор сервисов - не лучший подход для реализации IoC и агрегации. В некоторых случаях это антипаттерн. Следует использовать контейнер DI

person Pavel S    schedule 02.11.2016
comment
Извините .. набрал быстро и опубликовал с ошибками. Контейнеры DI следует использовать на верхнем уровне. Вы должны реализовать фабрику контроллеров и реализовать внедрение зависимостей через конструктор. Таким образом вы удалите избыточную зависимость от контейнера и получите явные зависимости, которые установлены в ctor. - person Pavel S; 02.11.2016