Cyb*_*axs 31 asp.net-mvc domain-driven-design onion-architecture
我正在学习Jeffrey Palermo着名的洋葱建筑.不是特定于这种模式,但我无法清楚地看到存储库和域服务之间的分离.我(错)了解存储库涉及数据访问和服务更多关于业务层(引用一个或多个存储库).
在许多示例中,存储库似乎具有某些类似于GetAllProductsByCategoryId
或的业务逻辑GetAllXXXBySomeCriteriaYYY
.
对于列表,似乎服务只是存储库中的包装器而没有任何逻辑.对于层次结构(父/子/子),它几乎是同一个问题:存储库的角色是加载完整的层次结构吗?
nwa*_*ng0 33
存储库不是访问数据库的网关.它是一种抽象,允许您从某种形式的持久性存储中存储和加载域对象.(数据库,缓存甚至普通集合).它接受或返回域对象而不是其内部字段,因此它是面向对象的接口.
建议不要在存储库中添加GetAllProductsByCategoryId
或使用某些方法GetProductByName
,因为随着用例/对象字段数量的增加,您将在存储库中添加越来越多的方法.相反,最好在存储库上有一个带有规范的查询方法.您可以传递规范的不同实现来检索产品.
总的来说,存储库模式的目标是创建一个存储抽象,在用例更改时不需要更改.本文详细讨论了域建模中的Repository模式.你可能感兴趣.
对于第二个问题:如果我ProductRepository
在代码中看到一个,我希望它返回一个Product列表.我还希望每个Product实例都是完整的.例如,如果Product有ProductDetail
对象的引用,我希望它Product.getDetail()
返回一个ProductDetail
实例而不是null.也许存储库的实现ProductDetail
与Product一起加载,也许该getDetail()
方法可以ProductDetailRepository
即时调用.我并不真正关心存储库的用户.我也可能只ProductDetail
在我打电话时返回一个id getDetail()
.从存储库的合同角度来看,这是完美的.然而,它使我的客户端代码变得复杂,并迫使我打电话给ProductDetailRepository
自己.
顺便说一句,我已经看过许多服务类,它们只包含我过去的存储库类.我认为这是一种反模式.最好让服务的调用者直接使用存储库.
cuo*_*gle 16
存储库模式使用类似集合的接口来访问域对象,从而在域和数据映射层之间进行调解.
因此,存储库是为域实体上的CRUD操作提供接口.请记住,存储库处理整个聚合.
聚合是属于一起的事物组.聚合根是将它们组合在一起的东西.
示例Order
和OrderLines
:
如果没有父订单,OrderLines没有理由存在,也不能属于任何其他订单.在这种情况下,Order和OrderLines可能是Aggregate,Order将是Aggregate Root
业务逻辑应该在域实体中,而不是在Repository层中,应用程序逻辑应该像您提到的那样在服务层中,这里的服务在存储库之间起协调作用.
虽然我仍然在努力解决这个问题,但我想发布一个答案,但我也接受(并希望)有关此问题的反馈.
在示例中 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()
}
Run Code Online (Sandbox Code Playgroud)
到现在为止还挺好.我们需要声明创建视图模型的"东西".让我们简化并说:
public IHttpActionResult Category(int id) {
var dependencies = service.GetDependenciesForCategory(id);
CategoryViewModel model = new CategoryViewModel(dependencies);
return View()
}
Run Code Online (Sandbox Code Playgroud)
好的,什么是依赖?我们可能需要类别树,产品,页面,总产品数量等.
所以如果我们以存储库的方式实现它,这看起来或多或少像这样:
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()
}
Run Code Online (Sandbox Code Playgroud)
相反,回到服务:
public IHttpActionResult Category(int id) {
var category = service.GetCategory(id);
if (category == null) return NotFound(); //
var model = new CategoryViewModel(category);
return View(model);
}
Run Code Online (Sandbox Code Playgroud)
好多了,但内心究竟是什么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;
}
}
Run Code Online (Sandbox Code Playgroud)
让我们尝试另一种方法,即工作单元,我将使用Entity框架作为UoW和Repositories,因此无需创建它们.
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;
}
}
Run Code Online (Sandbox Code Playgroud)
所以这里我们使用'query'语法而不是方法语法,但是我们可以使用我们的ORM而不是实现我们自己的复合体.此外,我们可以访问所有存储库,因此我们仍然可以在我们的服务中执行我们的工作单元.
现在我们需要选择我们想要的数据,我可能不想要我的实体的所有字段.
我能看到的最好的地方实际上是在ViewModel上,每个ViewModel可能需要映射它自己的数据,所以让我们再次改变服务的实现.
public CategoryService(DbContext db) {
// same dependency injection here
public Category GetCategory(int id) {
var category = db.Category.Find(id);
return category;
}
}
Run Code Online (Sandbox Code Playgroud)
那么所有的产品和内部类别在哪里?
让我们来看看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.
}
Run Code Online (Sandbox Code Playgroud)
你CategoryProductViewModel
现在可以独自想象.
但是(为什么总有一个但是??)
我们正在进行3 db命中,因为Find,我们正在获取所有类别字段.还必须启用延迟加载.不是真正的解决方案吗?
为了改善这一点,我们可以改变find到哪里......但是这会将Single
或者委托Find
给ViewModel,它也会返回一个IQueryable<Category>
,我们知道它应该只是一个.
记得我说过"我还在苦苦挣扎?" 这主要是为什么.要解决这个问题,我们应该从服务中返回确切需要的数据(也就是说......你知道它......是的!ViewModel).
所以让我们回到我们的控制器:
public IHttpActionResult Category(int id) {
var model = service.GetProductCategoryViewModel(id);
if (category == null) return NotFound(); //
return View(model);
}
Run Code Online (Sandbox Code Playgroud)
在GetProductCategoryViewModel
方法内部,我们可以调用返回不同部分的私有方法,并将它们组装为ViewModel.
这很糟糕,现在我的服务知道了viewmodels ...让我们解决这个问题.
我们创建一个接口,这个接口是这个方法将返回的实际合约.
ICategoryWithProductsAndChildsIds // quite verbose, i know.
Run Code Online (Sandbox Code Playgroud)
很好,现在我们只需要声明我们的ViewModel
public class CategoryViewModel : ICategoryWithProductsAndChildsIds
Run Code Online (Sandbox Code Playgroud)
并按照我们想要的方式实施它.
接口看起来有太多的东西,当然也可以与分裂ICategoryBasic
,IProducts
,IChilds
,或任何你可能要命名的.
所以当我们实现另一个viewModel时,我们可以选择只做IProducts
.我们可以让我们的服务具有方法(私有或非私有)来检索这些合同,并粘合服务层中的各个部分.(说起来容易做起来难)
当我进入一个完全正常工作的代码时,我可能会创建一个博客文章或一个github repo,但是现在,我还没有它,所以这就是现在.
我相信存储库应仅适用于CRUD操作.
public interface IRepository<T>
{
Add(T)
Remove(T)
Get(id)
...
}
Run Code Online (Sandbox Code Playgroud)
因此,IRepository将具有:添加,删除,更新,获取,GetAll以及可能的每个列表的版本,即AddMany,RemoveMany等.
要执行搜索检索操作,您应该有第二个接口,例如IFinder.您可以使用规范,因此IFinder可以使用Find(条件)方法来获取标准.或者你可以使用像IPersonFinder这样的东西来定义自定义函数,例如:FindPersonByName,FindPersonByAge等.
public interface IMyObjectFinder
{
FindByName(name)
FindByEmail(email)
FindAllSmallerThen(amount)
FindAllThatArePartOf(group)
...
}
Run Code Online (Sandbox Code Playgroud)
替代方案是:
public interface IFinder<T>
{
Find(criterias)
}
Run Code Online (Sandbox Code Playgroud)
第二种方法更复杂.您需要为标准定义策略.你打算使用某种类型的查询语言,还是使用更简单的键值关联等等.通过简单地查看,界面的全部功能也很难理解.使用此方法泄漏实现也更容易,因为标准可以基于特定类型的持久性系统,例如,如果您将SQL查询作为条件.另一方面,它可能会阻止您不得不连续回到IFinder,因为您遇到了需要更具体查询的特殊用例.我说它可能,因为您的标准策略不一定涵盖您可能需要的100%查询用例.
您还可以决定将两者混合在一起,并使用IFinder定义Find方法,使用IMyObjectFinders实现IFinder,还可以添加自定义方法,如FindByName.
该服务充当主管.假设您需要检索项目,但必须在将项目返回到客户端之前处理该项目,并且该处理可能需要在其他项目中找到的信息.因此,服务将使用存储库和Finder检索所有适当的项目,然后它将要处理的项目发送到封装必要处理逻辑的对象,最后它将返回客户端请求的项目.有时,不需要处理,也不需要额外的检索,在这种情况下,您不需要提供服务.您可以让客户直接调用存储库和Finder.这与Onion和分层架构有一个区别,在洋葱中,更多外部的东西可以更多地访问内部的所有内容,而不仅仅是它之前的层.
存储库的作用是加载正确构造它返回的项所需的完整层次结构.因此,如果您的存储库返回的项目具有另一种类型的项目的列表,那么它应该已经解决了这个问题.就个人而言,我喜欢设计我的对象,以便它们不包含对其他项的引用,因为它使存储库更复杂.我更喜欢让我的对象保留其他项的Id,这样如果客户端确实需要其他项,他可以使用给定Id的适当存储库再次查询它.这会使存储库返回的所有项目变得平坦,但如果需要,仍然可以创建层次结构.
如果您真的觉得有必要,可以在存储库中添加约束机制,以便您可以准确指定所需项目的哪个字段.假设你有一个人,只关心他的名字,你可以做Get(id,name),而且Repository不会费心去获取Person的每个字段,只有它的名字字段.尽管如此,这会给存储库增加相当大的复杂性.使用分层对象执行此操作甚至更复杂,尤其是如果要限制字段字段内的字段.所以我不推荐它.对我而言,唯一的好理由是性能至关重要,而且无法通过其他方式来改善性能.
归档时间: |
|
查看次数: |
14533 次 |
最近记录: |