洋葱架构 - 存储库与服务?

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自己.

顺便说一句,我已经看过许多服务类,它们只包含我过去的存储库类.我认为这是一种反模式.最好让服务的调用者直接使用存储库.

  • 你说不建议添加一些方法,如GetAllProductsByCategoryId或GetProductByName.如果不建议在存储库中编写这些方法,那么最佳位置是什么?是服务层吗? (10认同)
  • 使用预定义方法(如GetProductByCategoryId)与在Repository上使用规范查询是有争议的.有人说使用规范会使合同变得模糊,增加了复杂性.使用方法,您只需查看存储库,您就知道支持哪些用例.您可以更好地控制允许或不允许客户端的操作.根据规范,您可以使用可能不需要或不合适的案例.只要您向客户提供可能的查询,就永远无法收回,并且必须始终确保您的存储库支持它. (10认同)

cuo*_*gle 16

存储库模式使用类似集合的接口来访问域对象,从而在域和数据映射层之间进行调解.

因此,存储库是为域实体上的CRUD操作提供接口.请记住,存储库处理整个聚合.

聚合是属于一起的事物组.聚合根是将它们组合在一起的东西.

示例OrderOrderLines:

如果没有父订单,OrderLines没有理由存在,也不能属于任何其他订单.在这种情况下,Order和OrderLines可能是Aggregate,Order将是Aggregate Root

业务逻辑应该在域实体中,而不是在Repository层中,应用程序逻辑应该像您提到的那样在服务层中,这里的服务在存储库之间起协调作用.


Bar*_*xto 7

虽然我仍然在努力解决这个问题,但我想发布一个答案,但我也接受(并希望)有关此问题的反馈.

在示例中 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,但是现在,我还没有它,所以这就是现在.


Did*_* A. 6

我相信存储库应仅适用于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的每个字段,只有它的名字字段.尽管如此,这会给存储库增加相当大的复杂性.使用分层对象执行此操作甚至更复杂,尤其是如果要限制字段字段内的字段.所以我不推荐它.对我而言,唯一的好理由是性能至关重要,而且无法通过其他方式来改善性能.