32

コンテキスト / 質問

私は、データを永続化する必要があり、通常はリポジトリパターンを使用することになった多数の .NET プロジェクトに取り組んできました。コードベースのスケーラビリティを犠牲にすることなく、できるだけ多くの定型コードを削除するための優れた戦略を知っている人はいますか?

継承戦略

リポジトリ コードの多くはボイラー プレートであり、繰り返す必要があるため、通常、基本クラスを作成して、例外処理、ログ記録、トランザクション サポートなどの基本といくつかの基本的な CRUD メソッドをカバーします。

public abstract class BaseRepository<T> where T : IEntity
{
    protected void ExecuteQuery(Action query)
    {
        //Do Transaction Support / Error Handling / Logging
        query();
    }       

    //CRUD Methods:
    public virtual T GetByID(int id){}
    public virtual IEnumerable<T> GetAll(int id){}
    public virtual void Add (T Entity){}
    public virtual void Update(T Entity){}
    public virtual void Delete(T Entity){}
}

したがって、これは単純なドメインの場合にうまく機能し、エンティティごとに DRY リポジトリ クラスをすばやく作成できます。ただし、ドメインがより複雑になると、これは崩壊し始めます。更新を許可しない新しいエンティティが導入されたとしましょう。基本クラスを分割し、Update メソッドを別のクラスに移動できます。

public abstract class BaseRepositorySimple<T> where T : IEntity
{
    protected void ExecuteQuery(Action query);

    public virtual T GetByID(int id){}
    public virtual IEnumerable<T> GetAll(int id){}
    public virtual void Add (T entity){}
    public void Delete(T entity){}
}

public abstract class BaseRepositoryWithUpdate<T> :
    BaseRepositorySimple<T> where T : IEntity
{
     public virtual void Update(T entity){}
}

このソリューションはうまく拡張できません。一般的なメソッドを持つエンティティがいくつかあるとします: public virtual void Archive(T entity){}

ただし、アーカイブできる一部のエンティティは更新もできますが、他のエンティティはできません。したがって、私の継承ソリューションは機能しません。このシナリオに対処するには、2 つの新しい基本クラスを作成する必要があります。

構成戦略

Compositon パターンを調査しましたが、これには多くの定型コードが残っているようです。

public class MyEntityRepository : IGetByID<MyEntity>, IArchive<MyEntity>
{
    private Archiver<MyEntity> _archiveWrapper;      
    private GetByIDRetriever<MyEntity> _getByIDWrapper;

    public MyEntityRepository()
    {
         //initialize wrappers (or pull them in
         //using Constructor Injection and DI)
    }

    public MyEntity GetByID(int id)
    {
         return _getByIDWrapper(id).GetByID(id);
    }

    public void Archive(MyEntity entity)
    {
         _archiveWrapper.Archive(entity)'
    }
} 

MyEntityRepository にボイラープレート コードが読み込まれるようになりました。これを自動的に生成するために使用できるツール/パターンはありますか?

MyEntityRepository を次のようなものに変えることができれば、それははるかに理想的だと思います。

[Implement(Interface=typeof(IGetByID<MyEntity>), 
    Using = GetByIDRetriever<MyEntity>)]      
[Implement(Interface=typeof(IArchive<MyEntity>), 
    Using = Archiver<MyEntity>)
public class MyEntityRepository
{
    public MyEntityRepository()
    {
         //initialize wrappers (or pull them in
         //using Constructor Injection and DI)
    }
}

アスペクト指向プログラミング

これには AOP フレームワーク、特にPostSharpとそのComposition Aspectを使用することを検討しましたが、これでうまくいくように見えますが、リポジトリを使用するには Post.Cast<>() を呼び出す必要があります。コードに非常に奇妙な匂い。AOP を使用してコンポジターのボイラープレート コードを取り除くためのより良い方法があるかどうか、誰でも知っていますか?

カスタム コード ジェネレーター

他のすべてが失敗した場合は、ボイラー プレート コードを部分的なコード ファイルに生成できるカスタム コード ジェネレーター Visual Studio プラグインの作成に取り組むことができると思います。これを行うツールはすでにありますか?

[Implement(Interface=typeof(IGetByID<MyEntity>), 
    Using = GetByIDRetriever<MyEntity>)]      
[Implement(Interface=typeof(IArchive<MyEntity>), 
    Using = Archiver<MyEntity>)
public partial class MyEntityRepository
{
    public MyEntityRepository()
    {
         //initialize wrappers (or pull them in
         //using Constructor Injection and DI)
    }
} 

//Generated Class file
public partial class MyEntityRepository : IGetByID<MyEntity>, IArchive<MyEntity>
{
    private Archiver<MyEntity> _archiveWrapper;      
    private GetByIDRetriever<MyEntity> _getByIDWrapper;

    public MyEntity GetByID(int id)
    {
         return _getByIDWrapper(id).GetByID(id);
    }

    public void Archive(MyEntity entity)
    {
         _archiveWrapper.Archive(entity)'
    }
} 

拡張方法

最初に質問を書いたときにこれを追加するのを忘れていました(申し訳ありません)。また、拡張メソッドを試してみました:

public static class GetByIDExtenions
{
     public T GetByID<T>(this IGetByID<T> repository, int id){ }        
}

ただし、これには 2 つの問題があります。a) 拡張メソッド クラスの名前空間を覚えてどこにでも追加する必要があることと、b) 拡張メソッドがインターフェイスの依存関係を満たすことができないことです。

public interface IMyEntityRepository : IGetByID<MyEntity>{}
public class MyEntityRepository : IMyEntityRepository{}

更新: T4 テンプレートは可能な解決策でしょうか?

4

5 に答える 5

11

特定のデータ ストレージに対して 1 回だけ実装される単一の汎用リポジトリ インターフェイスがあります。ここにあります:

public interface IRepository<T> where T : class
{
    IQueryable<T> GetAll();
    T Get(object id);
    void Save(T item);
    void Delete(T item);
}

EntityFramework、NHibernate、RavenDB ストレージ用の実装があります。また、単体テスト用のメモリ内実装もあります。

たとえば、メモリ内コレクション ベースのリポジトリの一部を次に示します。

public class InMemoryRepository<T> : IRepository<T> where T : class
{
    protected readonly List<T> _list = new List<T>();

    public virtual IQueryable<T> GetAll()
    {
        return _list.AsReadOnly().AsQueryable();
    }

    public virtual T Get(object id)
    {
        return _list.FirstOrDefault(x => GetId(x).Equals(id));
    }

    public virtual void Save(T item)
    {
        if (_list.Any(x => EqualsById(x, item)))
        {
            Delete(item);
        }

        _list.Add(item);
    }

    public virtual void Delete(T item)
    {
        var itemInRepo = _list.FirstOrDefault(x => EqualsById(x, item));

        if (itemInRepo != null)
        {
            _list.Remove(itemInRepo);
        }
    }
}

汎用リポジトリ インターフェイスにより、類似したクラスを多数作成する必要がなくなりました。汎用リポジトリの実装は 1 つだけですが、クエリも自由です。

IQueryable<T>メソッドの結果をGetAll()使用すると、データに対して必要なクエリを作成し、それらをストレージ固有のコードから分離できます。すべての一般的な .NET ORM には独自の LINQ プロバイダーがあり、すべてそのマジックGetAll()メソッドを使用する必要があるため、ここでは問題ありません。

IoC コンテナーを使用して、コンポジション ルートでリポジトリの実装を指定します。

ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));

私が使用しているテストでは、メモリ内置換です:

ioc.Bind(typeof (IRepository<>)).To(typeof (InMemoryRepository<>));

リポジトリにビジネス固有のクエリをさらに追加する場合は、拡張メソッドを追加します (回答の拡張メソッドに似ています)。

public static class ShopQueries
{
    public IQueryable<Product> SelectVegetables(this IQueryable<Product> query)
    {
        return query.Where(x => x.Type == "Vegetable");
    }

    public IQueryable<Product> FreshOnly(this IQueryable<Product> query)
    {
        return query.Where(x => x.PackTime >= DateTime.Now.AddDays(-1));
    }
}

そのため、ビジネス ロジック層のクエリでこれらのメソッドを使用および混合して、次のようなリポジトリ実装のテスト容易性と容易さを節約できます。

var freshVegetables = repo.GetAll().SelectVegetables().FreshOnly();

これらの拡張メソッドに別の名前空間を使用したくない場合 (私のように) - OK、それらをリポジトリ実装が存在する同じ名前空間 ( などMyProject.Data) に配置するか、さらに良いことに、既存のビジネス固有の名前空間 (MyProject.Productsまたはなど) に配置します。 MyProject.Data.Products)。追加の名前空間を覚えておく必要はありません。

ある種のエンティティに対して特定のリポジトリ ロジックがある場合は、必要なメソッドをオーバーライドする派生リポジトリ クラスを作成します。たとえば、商品がProductNumber代わりにしか見つからIdず、削除をサポートしていない場合は、次のクラスを作成できます。

public class ProductRepository : RavenDbRepository<Product>
{
    public override Product Get(object id)
    {
        return GetAll().FirstOrDefault(x => x.ProductNumber == id);
    }

    public override Delete(Product item)
    {
        throw new NotSupportedException("Products can't be deleted from db");
    }
}

そして、IoC が製品用のこの特定のリポジトリ実装を返すようにします。

ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));
ioc.Bind<IRepository<Product>>().To<ProductRepository>();

それが私のリポジトリをバラバラにする方法です;)

于 2013-03-16T22:42:12.560 に答える
4

コード生成用の T4 ファイルをチェックアウトします。T4 は Visual Studio に組み込まれています。こちらのチュートリアルを参照してください

LINQ DBML を検査して POCO エンティティを生成するコード用の T4 ファイルを作成しました。それらのリポジトリ用に、ここで役立つと思います。T4 ファイルを使用して部分クラスを生成する場合は、特殊なケースのコードを記述するだけで済みます。

于 2013-04-11T19:32:50.833 に答える
2

私には、基本クラスを分割し、両方の機能を 1 つの継承クラスに入れたいと思われます。そのような場合は、構成が選択されます。複数のクラス継承も、C# でサポートされている場合は便利です。ただし、継承の方が優れており、再利用性も問題ないと感じているため、最初のオプションの選択はそれで済みます。

オプション1

2つの構成ではなく、もう1つの基本クラスが必要です。再利用性は、継承ではなく静的メソッドでも解決できます。

リユース部は外から見えません。名前空間を覚える必要はありません。

static class Commons
{
    internal static void Update(/*receive all necessary params*/) 
    { 
        /*execute and return result*/
    }

    internal static void Archive(/*receive all necessary params*/) 
    { 
        /*execute and return result*/
    }
}

class Basic 
{
    public void SelectAll() { Console.WriteLine("SelectAll"); }
}

class ChildWithUpdate : Basic
{
    public void Update() { Commons.Update(); }
}

class ChildWithArchive : Basic
{
    public void Archive() { Commons.Archive(); }
}

class ChildWithUpdateAndArchive: Basic
{
    public void Update() { Commons.Update(); }
    public void Archive() { Commons.Archive(); }
}

もちろん、マイナーな繰り返しコードもありますが、それは共通ライブラリから既製の関数を呼び出しているだけです。

オプション 2

構成の私の実装(または多重継承の模倣):

public class Composite<TFirst, TSecond>
{
    private TFirst _first;
    private TSecond _second;

    public Composite(TFirst first, TSecond second)
    {
        _first = first;
        _second = second;
    }

    public static implicit operator TFirst(Composite<TFirst, TSecond> @this)
    {
        return @this._first;
    }

    public static implicit operator TSecond(Composite<TFirst, TSecond> @this)
    {
        return @this._second;
    }

    public bool Implements<T>() 
    {
        var tType = typeof(T);
        return tType == typeof(TFirst) || tType == typeof(TSecond);
    }
}

継承と構成 (以下):

class Basic 
{
    public void SelectAll() { Console.WriteLine("SelectAll"); }
}

class ChildWithUpdate : Basic
{
    public void Update() { Console.WriteLine("Update"); }
}

class ChildWithArchive : Basic
{
    public void Archive() { Console.WriteLine("Archive"); }
}

構成。定型コードが存在しないと言うのにこれで十分かどうかはわかりません。

class ChildWithUpdateAndArchive : Composite<ChildWithUpdate, ChildWithArchive>
{
    public ChildWithUpdateAndArchive(ChildWithUpdate cwu, ChildWithArchive cwa)
        : base(cwu, cwa)
    {
    }
}

これらすべてを使用するコードは一見問題ないように見えますが、代入での通常とは異なる (目に見えない) 型キャストが行われます。これは、ボイラープレート コードが少ないことの見返りです。

ChildWithUpdate b;
ChildWithArchive c;
ChildWithUpdateAndArchive d;

d = new ChildWithUpdateAndArchive(new ChildWithUpdate(), new ChildWithArchive());
//now call separated methods.
b = d;
b.Update();
c = d;
c.Archive();
于 2013-04-17T19:59:00.183 に答える
1

ビジター パターンを使用できます。ここで実装を読んで、必要な機能のみを実装できます。

これがアイデアです:

public class Customer : IAcceptVisitor
{
    private readonly string _id;
    private readonly List<string> _items = new List<string>();
    public Customer(string id)
    {
        _id = id;
    }

    public void AddItems(string item)
    {
        if (item == null) throw new ArgumentNullException(nameof(item));
        if(_items.Contains(item)) throw new InvalidOperationException();
        _items.Add(item);
    }

    public void Accept(ICustomerVisitor visitor)
    {
        if (visitor == null) throw new ArgumentNullException(nameof(visitor));
        visitor.VisitCustomer(_items);
    }
}
public interface IAcceptVisitor
{
    void Accept(ICustomerVisitor visitor);
}

public interface ICustomerVisitor
{
    void VisitCustomer(List<string> items);
}

public class PersistanceCustomerItemsVisitor : ICustomerVisitor
{
    public int Count { get; set; }
    public List<string> Items { get; set; }
    public void VisitCustomer(List<string> items)
    {
        if (items == null) throw new ArgumentNullException(nameof(items));
        Count = items.Count;
        Items = items;
    }
}

そのため、永続化のためにビジター パターンを適用して、ドメイン ロジックとインフラストラクチャの間に関心の分離を適用できます。よろしく!

于 2016-12-05T18:32:19.230 に答える
1

これが私のバージョンです:

interface IGetById
{
    T GetById<T>(object id);
}

interface IGetAll
{
    IEnumerable<T> GetAll<T>();
}

interface ISave
{
    void Save<T>(T item) where T : IHasId; //you can go with Save<T>(object id, T item) if you want pure pure POCOs
}

interface IDelete
{
    void Delete<T>(object id);
}

interface IHasId
{
    object Id { get; set; }
}

追加の制限があり、後で作業するのが難しくなるため、一般的なリポジトリ インターフェイスは好きではありません。代わりに一般的な方法を使用します。

リポジトリにヘッダー インターフェイスを使用する代わりに、各リポジトリ メソッドにロール インターフェイスを使用します。これにより、ロギング、PubSub への変更の公開など、リポジトリ メソッドに追加機能を追加できます。

どのデータベースにも適合する優れたシンプルなクエリの抽象化をまだ見つけていないため、カスタムクエリにはリポジトリを使用しません。私のバージョンのリポジトリは、ID でのみアイテムを取得するか、同じタイプのすべてのアイテムを取得できます。他のクエリはメモリ内で実行されます (パフォーマンスが十分に優れている場合)、または他のメカニズムがあります。

利便性のために IRepository インターフェイスを導入できるため、crud コントローラーなどのために 4 つのインターフェイスを常に記述する必要はありません。

interface IRepository : IGetById, IGetAll, ISave, IDelete { }

class Repository : IRepository
{
    private readonly IGetById getter;

    private readonly IGetAll allGetter;

    private readonly ISave saver;

    private readonly IDelete deleter;

    public Repository(IGetById getter, IGetAll allGetter, ISave saver, IDelete deleter)
    {
        this.getter = getter;
        this.allGetter = allGetter;
        this.saver = saver;
        this.deleter = deleter;
    }

    public T GetById<T>(object id)
    {
        return getter.GetById<T>(id);
    }

    public IEnumerable<T> GetAll<T>()
    {
        return allGetter.GetAll<T>();
    }

    public void Save<T>(T item) where T : IHasId
    {
        saver.Save(item);
    }

    public void Delete<T>(object id)
    {
        deleter.Delete<T>(id);
    }
}

ロール インターフェイスを使用すると、追加の動作を追加できると述べました。デコレータを使用した例をいくつか示します。

class LogSaving : ISave
{
    private readonly ILog logger;

    private readonly ISave next;

    public LogSaving(ILog logger, ISave next)
    {
        this.logger = logger;
        this.next = next;
    }

    public void Save<T>(T item) where T : IHasId
    {
        this.logger.Info(string.Format("Start saving {0} : {1}", item.ToJson()));
        next.Save(item);
        this.logger.Info(string.Format("Finished saving {0}", item.Id));
    }
}

class PublishChanges : ISave, IDelete
{
    private readonly IPublish publisher;

    private readonly ISave nextSave;

    private readonly IDelete nextDelete;

    private readonly IGetById getter;

    public PublishChanges(IPublish publisher, ISave nextSave, IDelete nextDelete, IGetById getter)
    {
        this.publisher = publisher;
        this.nextSave = nextSave;
        this.nextDelete = nextDelete;
        this.getter = getter;
    }

    public void Save<T>(T item) where T : IHasId
    {
        nextSave.Save(item);
        publisher.PublishSave(item);
    }

    public void Delete<T>(object id)
    {
        var item = getter.GetById<T>(id);
        nextDelete.Delete<T>(id);
        publisher.PublishDelete(item);
    }
}

テストのためにメモリストアに実装するのは難しくありません

class InMemoryStore : IRepository
{
    private readonly IDictionary<Type, Dictionary<object, object>> db;

    public InMemoryStore(IDictionary<Type, Dictionary<object, object>> db)
    {
        this.db = db;
    }

    ...
}

最後に全部まとめて

var db = new Dictionary<Type, Dictionary<object, object>>();
var store = new InMemoryStore(db);
var storePublish = new PublishChanges(new Publisher(...), store, store, store);
var logSavePublish = new LogSaving(new Logger(), storePublish);
var repo = new Repository(store, store, logSavePublish, storePublish);
于 2014-11-25T11:21:33.263 に答える