web-dev-qa-db-ja.com

オニオンアーキテクチャ、作業単位、および一般的なリポジトリパターン

よりドメイン駆動の設計アプローチを実装するのはこれが初めてです。 Onion Architecture はインフラストラクチャ/プラットフォームなどではなくドメインに焦点を当てているため、試してみることにしました。

enter image description here

Entity Frameworkから抽象化するために、Unit of Work実装を使用してgenericリポジトリを作成しました。

IRepository<T>およびIUnitOfWorkインターフェース:

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

    void Remove(T item);

    IQueryable<T> Query();
}

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

IRepository<T>およびIUnitOfWorkのEntityFrameworkの実装:

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

Customerリポジトリ:

public interface ICustomerRepository : IRepository<Customer>
{

}

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

リポジトリを使用するASP.NETMVCコントローラー:

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クラスは基本的に空です。それらは純粋に抽象化を提供するためにあります。私の知る限り、これは、インフラストラクチャとプラットフォームに依存するコードがシステムの外部にあるOnionアーキテクチャのビジョンと一致しますが、空のクラスと空のインターフェイスがあると間違っていると感じますか?

  • 別の永続性の実装(Azure Table Storageなど)を使用するには、新しいCustomerRepositoryクラスを作成する必要があり、AzureTableStorageRepository<T>を継承します。しかし、これは冗長なコード(複数のCustomerRepositories)につながる可能性がありますか?これはモックにどのように影響しますか?

  • 別の実装(たとえば、Azure Table Storage)には、国境を越えたサポートに制限があるため、AzureTableStorageUnitOfWorkクラスはこのコンテキストでは機能しません。

これを行った方法に他に問題はありますか?

(私は私のインスピレーションのほとんどを この投稿 から取っています)

19
Dave New

このコードは初めて試すのに十分であると言えますが、改善すべき点がいくつかあります。

それらのいくつかを見てみましょう。

1。依存性注入(DI)とIoCの使用法

Service Locator pattern --containerインスタンス自体の最も単純なバージョンを使用します。

「コンストラクタインジェクション」を使用することをお勧めします。詳細については、 ここ(ASP.NET MVC 4依存性注入) を参照してください。

_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)スコープ。

IUnitOfWorkICustomerRepositoryのライフスタイルが見つかりません。私は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のインスタンスとは異なると思います。バグが発生する場合があります。ただし、Unityの問題として ASP.NET MVC 4依存性注入 では強調表示されていません。

Castle WindsorPerWebRequest ライフスタイルは、単一のhttpリクエスト内で同じタイプのインスタンスを共有するために使用されます。

UnitOfWorkPerWebRequest コンポーネントである場合、これは一般的なアプローチです。カスタムActionFilterは、Commit()メソッドの呼び出し中にOnActionExecuted()を呼び出すために使用できます。

また、SaveChanges()メソッドの名前を変更し、 example および PoEAA で呼び出されるので、単にCommitと呼びます。

_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();
    }
}
_
23
Ilya Palkin

私が見る唯一の欠点は、あなたがIOCツールに大きく依存しているということです。したがって、実装がしっかりしていることを確認してください。ただし、これはOnionの設計に固有のものではありません。私はいくつかのOnionを使用しました。プロジェクトの数であり、実際の「落とし穴」に遭遇したことはありません。

2
Maess

コードにいくつかの深刻な問題があります。

最初の問題は、リポジトリとUoWの間の関係です。

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

これが暗黙の依存関係です。リポジトリはUoWなしでは機能しません!すべてのリポジトリをUoWに接続する必要はありません。たとえば、ストアドプロシージャはどうですか?ストアドプロシージャがあり、リポジトリの背後に隠しています。ストアドプロシージャの呼び出しでは、個別のトランザクションが使用されます。少なくともすべての場合ではありません。したがって、唯一のリポジトリを解決してアイテムを追加すると、機能しなくなります。さらに、リポジトリには別のUoWインスタンスがあるため、一時的なライフライセンスを設定すると、このコードは機能しません。したがって、緊密な暗黙の結合があります。

DIコンテナエンジン間に密結合を作成し、それをサービスロケーターとして使用する2番目の問題!サービスロケーターは、IoCと集約を実装するための適切なアプローチではありません。場合によってはアンチパターンです。 DIコンテナを使用する必要があります

0
Pavel S