私は有名なオニオンアーキテクチャをジェフリーパレルモから学んでいます。このパターンに固有ではありませんが、リポジトリとドメインサービスの分離は明確にはわかりません。リポジトリがデータアクセスとサービスに関係していることはビジネスレイヤーに関するものだと誤解しています(1つ以上のリポジトリを参照してください)。
多くの例では、リポジトリにはGetAllProductsByCategoryId
やGetAllXXXBySomeCriteriaYYY
のようなビジネスロジックが背後にあるようです。
リストの場合、サービスはロジックのない単なるリポジトリのラッパーのようです。階層(親/子/子)の場合、それはほとんど同じ問題です。完全な階層をロードするのはリポジトリの役割ですか?
リポジトリは、データベースにアクセスするためのゲートウェイではありません。これは、なんらかの形式の永続ストアからドメインオブジェクトを格納およびロードできるようにする抽象化です。 (データベース、キャッシュ、またはプレーンなコレクション)。内部フィールドではなくドメインオブジェクトを取得または返すため、オブジェクト指向のインターフェイスです。
GetAllProductsByCategoryId
やGetProductByName
などのいくつかのメソッドをリポジトリに追加することはお勧めしません。ユースケース/オブジェクトフィールドの数が増えるにつれて、リポジトリにメソッドを追加していくからです。代わりに、仕様を取得するリポジトリにクエリメソッドを設定することをお勧めします。仕様のさまざまな実装を渡して製品を取得できます。
全体として、リポジトリパターンの目標は、ユースケースの変更時に変更を必要としないストレージ抽象化を作成することです。 この記事 は、ドメインモデリングにおけるリポジトリパターンについて非常に詳細に説明しています。あなたは興味があるかもしれません。
2番目の質問:コードにProductRepository
がある場合、Productのリストが返されると思います。また、各Productインスタンスが完成していることも期待しています。たとえば、ProductがProductDetail
オブジェクトへの参照を持っている場合、Product.getDetail()
がnullではなくProductDetail
インスタンスを返すと思います。多分リポジトリロードProductDetail
とProductの実装、多分getDetail()
メソッドはProductDetailRepository
をその場で呼び出します。リポジトリのユーザーとして私は本当に気にしません。また、getDetail()
を呼び出したときに、製品がProductDetail
idのみを返す可能性もあります。リポジトリの契約の観点からは、これは完璧です。ただし、クライアントコードが複雑になり、ProductDetailRepository
を自分で呼び出す必要があります。
ちなみに、過去にはリポジトリクラスをラップするだけのサービスクラスがたくさんありました。アンチパターンだと思います。サービスの呼び出し元がリポジトリを直接使用することをお勧めします。
リポジトリパターンは、ドメインオブジェクトにアクセスするためのコレクションのようなインターフェイスを使用して、ドメインとデータマッピングレイヤーの間を仲介します。
したがって、リポジトリーは、ドメイン・エンティティーでのCRUD操作のインターフェースを提供することです。リポジトリはAggregate全体を扱うことに注意してください。
集合体は、一緒に属するもののグループです。集約ルートは、それらすべてをまとめて保持するものです。
例Order
およびOrderLines
:
OrderLineは、親Orderなしで存在する理由はなく、他のOrderに属することもできません。この場合、OrderとOrderLinesはおそらくAggregateであり、OrderはAggregate Rootです
ビジネスロジックは、リポジトリレイヤーではなくドメインエンティティにある必要があります。アプリケーションロジックは、前述のようにサービスレイヤーにある必要があります。ここのサービスは、リポジトリ間のコーディネーターとしての役割を果たします。
私はまだこれに苦労していますが、私は回答として投稿したいですが、これについてのフィードバックも受け入れます(欲しい)。
例ではGetProductsByCategory(int id)
まず、最初の必要から考えましょう。コントローラー、おそらくCategoryControllerにぶつかったので、
_public CategoryController(ICategoryService service) {
// here we inject our service and keep a private variable.
}
public IHttpActionResult Category(int id) {
CategoryViewModel model = something.GetCategoryViewModel(id);
return View()
}
_
ここまでは順調ですね。ビューモデルを作成する「何か」を宣言する必要があります。簡単に言ってみましょう:
_public IHttpActionResult Category(int id) {
var dependencies = service.GetDependenciesForCategory(id);
CategoryViewModel model = new CategoryViewModel(dependencies);
return View()
}
_
依存関係とは何ですか?私たちはたぶんカテゴリツリー、製品、ページ、製品の合計数などが必要です。
したがって、これをリポジトリの方法で実装した場合、これは多かれ少なかれ次のようになります。
_public IHttpActionResult Category(int id) {
var products = repository.GetCategoryProducts(id);
var category = repository.GetCategory(id); // full details of the category
var childs = repository.GetCategoriesSummary(category.childs);
CategoryViewModel model = new CategoryViewModel(products, category, childs); // awouch!
return View()
}
_
代わりに、サービスに戻ります。
_public IHttpActionResult Category(int id) {
var category = service.GetCategory(id);
if (category == null) return NotFound(); //
var model = new CategoryViewModel(category);
return View(model);
}
_
はるかに良いですが、正確に何がservice.GetCategory(id)
の内側にありますか?
_public CategoryService(ICategoryRespository categoryRepository, IProductRepository productRepository) {
// same dependency injection here
public Category GetCategory(int id) {
var category = categoryRepository.Get(id);
var childs = categoryRepository.Get(category.childs) // int[] of ids
var products = productRepository.GetByCategory(id) // this doesn't look that good...
return category;
}
}
_
別のアプローチ、つまり作業単位を試してみましょう。UoWとリポジトリとしてEntityフレームワークを使用するので、それらを作成する必要はありません。
_public CategoryService(DbContext db) {
// same dependency injection here
public Category GetCategory(int id) {
var category = db.Category.Include(c=> c.Childs).Include(c=> c.Products).Find(id);
return category;
}
}
_
したがって、ここではメソッド構文の代わりに「クエリ」構文を使用していますが、独自のコンプレックスを実装する代わりに、ORMを使用できます。また、すべてのリポジトリにアクセスできるため、サービス内で引き続き作業単位を実行できます。
次に、必要なデータを選択する必要があります。エンティティのすべてのフィールドが必要なわけではありません。
これが発生しているのを確認できる最適な場所は、実際にはViewModelです。各ViewModelは独自のデータをマップする必要がある場合があるため、サービスの実装を再度変更します。
_public CategoryService(DbContext db) {
// same dependency injection here
public Category GetCategory(int id) {
var category = db.Category.Find(id);
return category;
}
}
_
それで、すべての製品と内部カテゴリーはどこにありますか?
viewModelを見てみましょう。これはデータを値にマップするだけであることに注意してください。ここで何か他のことをしている場合は、おそらくViewModelにあまりにも多くの責任を与えていることになります。
_public CategoryViewModel(Category category) {
Name = category.Name;
Id = category.Id;
Products = category.Products.Select(p=> new CategoryProductViewModel(p));
Childs = category.Childs.Select(c => c.Name); // only childs names.
}
_
あなたはCategoryProductViewModel
を自分で今想像することができます。
しかし(なぜいつもあるのですか??)
3つのdbヒットを実行しており、検索のためにすべてのカテゴリフィールドをフェッチしています。また、遅延読み込み必須有効にする必要があります。実際の解決策ではありませんか?
これを改善するために、whereを使用してfindを変更できます...しかし、これはSingle
またはFind
をViewModelに委任します。また、わかっている場所で_IQueryable<Category>
_を返します。正確に1でなければなりません。
「まだ苦労している」と言ったことを思い出してください。これが主な理由です。これを修正するには、サービスから正確に必要なデータを返す必要があります(別名.....知っています....はい、ViewModel)。
コントローラに戻りましょう:
_public IHttpActionResult Category(int id) {
var model = service.GetProductCategoryViewModel(id);
if (category == null) return NotFound(); //
return View(model);
}
_
GetProductCategoryViewModel
メソッド内で、さまざまな部分を返し、それらをViewModelとしてアセンブルするプライベートメソッドを呼び出すことができます。
これは悪いですが、私のサービスはビューモデルについて知っています...それを修正しましょう。
インターフェイスを作成します。このインターフェイスは、このメソッドが返すものの実際のコントラクトです。
_ICategoryWithProductsAndChildsIds // quite verbose, i know.
_
これで、ViewModelを次のように宣言するだけで済みます。
_public class CategoryViewModel : ICategoryWithProductsAndChildsIds
_
そして私たちが望む方法でそれを実装します。
インターフェースは多すぎるように見えますが、もちろんICategoryBasic
、IProducts
、IChilds
、またはそれらに名前を付けたいと思われるもので分割できます。
したがって、別のviewModelを実装する場合、IProducts
のみを実行するように選択できます。これらのコントラクトを取得するメソッド(プライベートかどうかに関係なく)をサービスに持たせ、サービスレイヤーの一部を接着することができます。 (言うより簡単)
完全に機能するコードに入ると、ブログ投稿またはgithubリポジトリを作成する可能性がありますが、現時点ではまだ持っていないため、これですべてです。
リポジトリはCRUDオペレーション専用であるべきだと思います。
public interface IRepository<T>
{
Add(T)
Remove(T)
Get(id)
...
}
したがって、IRepositoryには、Add、Remove、Update、Get、GetAll、そして場合によっては、リストを取得するそれぞれのバージョン(AddMany、RemoveManyなど)があります。
検索取得操作を実行するには、IFinderなどの2番目のインターフェースが必要です。仕様を使用することもできるので、IFinderは基準を取得するFind(criteria)メソッドを持つことができます。または、FindPersonByName、FindPersonByAgeなどのカスタム関数を定義するIPersonFinderのようなものを使用することもできます。
public interface IMyObjectFinder
{
FindByName(name)
FindByEmail(email)
FindAllSmallerThen(amount)
FindAllThatArePartOf(group)
...
}
代替案は次のとおりです。
public interface IFinder<T>
{
Find(criterias)
}
この2番目のアプローチはより複雑です。基準の戦略を定義する必要があります。なんらかのクエリ言語を使用するのでしょうか、それとも、より単純なキーと値の関連付けなどを使用するのでしょうか。インターフェイスをフルに活用しても、単純に見ただけでは理解が難しくなります。たとえば、SQLクエリを条件として使用する場合のように、特定のタイプの永続化システムに基づいて条件を設定できるため、このメソッドを使用して実装をリークするのも簡単です。一方、より具体的なクエリを必要とする特殊なユースケースに遭遇したために、IFinderに継続的に戻る必要がなくなる可能性があります。基準戦略は必ずしも必要なクエリのユースケースの100%をカバーするわけではないので、そうかもしれません。
また、両方を組み合わせて、Findメソッドを定義するIFinderとIFinderを実装するIMyObjectFindersを用意するだけでなく、FindByNameなどのカスタムメソッドを追加することもできます。
サービスはスーパーバイザーとして機能します。たとえば、アイテムを取得する必要があるが、クライアントに返す前にアイテムを処理する必要があるとします。その処理には、他のアイテムにある情報が必要になる場合があります。そのため、サービスはリポジトリとファインダーを使用してすべての適切なアイテムを取得し、処理するアイテムを必要な処理ロジックをカプセル化するオブジェクトに送信し、最後にクライアントから要求されたアイテムを返します。場合によっては、処理や追加の検索は必要ありません。そのような場合は、サービスを利用する必要はありません。クライアントにリポジトリとファインダーを直接呼び出させることができます。これは、オニオンとレイヤードアーキテクチャの1つの違いです。オニオンでは、より外側にあるすべてのものは、その前のレイヤーだけでなく、より内側にあるすべてのものにアクセスできます。
それが返すアイテムを適切に構築するために必要なものの完全な階層をロードするのはリポジトリの役割です。したがって、リポジトリが別のタイプのアイテムのリストを持つアイテムを返す場合、それはすでにそれを解決しているはずです。個人的には、リポジトリをより複雑にするため、他のアイテムへの参照を含まないようにオブジェクトを設計するのが好きです。私は自分のオブジェクトに他のアイテムのIDを保持させたいので、クライアントが本当に他のアイテムを必要とする場合、IDが与えられた適切なリポジトリを使用して再度クエリできます。これにより、リポジトリから返されるすべてのアイテムが平坦化されますが、必要に応じて階層を作成できます。
本当に必要があると感じた場合は、リポジトリに制限メカニズムを追加して、必要なアイテムのフィールドを正確に指定できるようにすることができます。 Personがいて、彼の名前だけを気にしているとしましょう。Get(id、name)を実行すると、リポジトリはPersonのすべてのフィールドを取得するのではなく、名前フィールドだけを取得します。ただし、これを行うと、リポジトリがかなり複雑になります。そして、特にフィールドのフィールド内のフィールドを制限したい場合は、階層オブジェクトでこれを行うことはさらに複雑です。だから私は本当にそれをお勧めしません。私にとってこれの唯一の正当な理由は、パフォーマンスが重要であり、パフォーマンスを改善するために他に何もできない場合です。
ドメイン駆動設計では、リポジトリはアグリゲート全体を取得する責任があります。