2013-12-09 4 views
18

Это первый раз, когда я внедряю более основанный на доменах подход к дизайну. Я решил попробовать 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>(); 

Solution:

enter image description here

ПРОБЛЕМЫ?

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

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

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

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

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

(я взял большую часть моего вдохновения от this post)

+2

Вы используете [анти-шаблон шаблона службы] (http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/), принимая зависимость от контейнера IoC. Вместо этого вы должны зарегистрировать фабричный класс для ввода в ваш контроллер. Некоторые контейнеры IoC могут создавать один из них для вас в форме 'Func ' dependency – AlexFoxGill

ответ

22

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

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

1. Зависимость впрыска (DI) и использование IoC

используется простейший вариант Service Locator pattern -... container сам, например

Я предлагаю вам использовать «инъекцию конструктора». Вы можете найти дополнительную информацию here (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 says that TransientLifetimeManager is used by default. Это означает, что вы будете получать новый экземпляр каждый раз при разрешении типа.

Таким образом, следующий тест не пройден:

[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 WindsorPerWebRequest Образ жизни используется для совместного использования одного экземпляра типа в рамках одного запроса HTTP.

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

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

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

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(); 
    } 
} 
+0

Спасибо за отличный ответ. (1) Совершено (2) Я боролся с этой проблемой. Теперь, используя PerResolveLifetimeManager (3.1), я теперь использую конкретные интерфейсы для определенных методов выборки (например, GetCustomerByName). Я решил удалить метод IQueryable Query() из репозиториев, поскольку он просачивал детали EF в код клиента. (3.2) Я рассмотрю это ближе. – davenewza

2

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

+0

Не является ли IoC ключевым для работы Onion? Я мог бы объявить конкретные классы в моем кодовом коде, но тогда я значительно увеличиваю связь и уменьшу тестируемость? Спасибо за ответ :) – davenewza

+1

Да, поэтому я говорю, что его реализация должна быть твердой. Вам нужно использовать DI и IOC, чтобы сделать работу Onion. – Maess

0

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

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

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

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

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

+0

Извините, быстро напечатал и опубликовал с ошибками. Контейнеры DI следует использовать на верхнем уровне.Вы должны внедрить контроллер на заводе и внедрить инъекцию зависимостей через contructor. Используя этот способ, вы удалите избыточную зависимость от контейнера и получите явные зависимости, установленные в ctor. –

Смежные вопросы