Подтвердить что ты не робот

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

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

enter image description here

Чтобы абстрагироваться от 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>();

Решение:

enter image description here

ПРОБЛЕМЫ?

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

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

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

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

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

(Я взял большую часть своего вдохновения из этого сообщения)

4b9b3361

Ответ 1

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

Пропустите некоторые из них.

1. Инъекция зависимостей (DI) и использование IoC.

Вы используете простейшую версию шаблон локатора службы - сам экземпляр 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 используется по умолчанию. Это означает, что вы будете получать новый экземпляр каждый раз при разрешении типа.

Итак, следующий тест не выполняется:

[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.

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

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

Я бы также переименовал метод SaveChanges() и назвал его просто Commit, как он вызывается в , а в 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();
    }
}

Ответ 2

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

Ответ 3

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

Первая проблема связана с репозиториями и UoW.

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

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

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