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

时间:2012-09-07 11:21:46

标签: asp.net-mvc domain-driven-design onion-architecture

我正在学习Jeffrey Palermo着名的洋葱建筑。 不是特定于这种模式,但我无法清楚地看到存储库和域服务之间的分离。 我(错误)了解存储库涉及数据访问和服务更多关于业务层(引用一个或多个存储库)。

在许多示例中,存储库似乎具有某种业务逻辑,例如GetAllProductsByCategoryIdGetAllXXXBySomeCriteriaYYY

对于列表,似乎服务只是存储库中的包装器而没有任何逻辑。 对于层次结构(父/子/子),它几乎是同一个问题:存储库的角色是加载完整的层次结构吗?

7 个答案:

答案 0 :(得分:32)

存储库不是访问数据库的网关。它是一种抽象,允许您从某种形式的持久性存储中存储和加载域对象。 (数据库,缓存甚至普通集合)。它接受或返回域对象而不是其内部字段,因此它是面向对象的接口。

不建议将某些方法(如GetAllProductsByCategoryIdGetProductByName)添加到存储库中,因为随着用例/对象字段数量的增加,您将在存储库中添加越来越多的方法。相反,最好在存储库上有一个带有规范的查询方法。您可以传递规范的不同实现来检索产品。

总的来说,存储库模式的目标是创建一个存储抽象,在用例更改时不需要更改。 This article非常详细地讨论了域建模中的Repository模式。你可能感兴趣。

对于第二个问题:如果我在代码中看到ProductRepository,我希望它会返回一个Product列表。我还希望每个Product实例都是完整的。例如,如果Product有ProductDetail对象的引用,我希望Product.getDetail()返回ProductDetail实例而不是null。也许存储库的实现与Product一起加载ProductDetail,也许getDetail()方法即时调用ProductDetailRepository。我并不真正关心存储库的用户。当我致电ProductDetail时,产品也可能只返回getDetail() ID。从存储库的合同角度来看,这是完美的。但是它使我的客户端代码变得复杂,迫使我自己打电话给ProductDetailRepository

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

答案 1 :(得分:15)

  

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

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

  

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

示例OrderOrderLines

  

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

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

答案 2 :(得分:6)

我认为存储库应仅用于CRUD操作。

public interface IRepository<T>
{
    Add(T)
    Remove(T)
    Get(id)
    ...
}

所以IRepository会有:添加,删除,更新,获取,GetAll以及每个带有列表的版本,即AddMany,RemoveMany等。

要执行搜索检索操作,您应该有第二个界面,例如IFinder。您可以使用规范,因此IFinder可以使用Find(条件)方法来获取标准。或者你可以使用像IPersonFinder这样的东西来定义自定义函数,例如:FindPersonByName,FindPersonByAge等。

public interface IMyObjectFinder
{
    FindByName(name)
    FindByEmail(email)
    FindAllSmallerThen(amount)
    FindAllThatArePartOf(group)
    ...
}

替代方案是:

public interface IFinder<T>
{
    Find(criterias)
}

第二种方法更复杂。您需要为标准定义策略。你打算使用某种类型的查询语言,还是使用更简单的键值关联等等。通过简单地查看,界面的全部功能也很难理解。使用此方法泄漏实现也更容易,因为标准可以基于特定类型的持久性系统,例如,如果您将SQL查询作为条件。另一方面,它可能会阻止您不得不连续回到IFinder,因为您遇到了需要更具体查询的特殊用例。我说这可能,因为您的标准策略不一定涵盖您可能需要的100%查询用例。

您还可以决定将两者混合在一起,并使用IFinder定义Find方法,使用IMyObjectFinders实现IFinder,还可以添加自定义方法,例如FindByName。

该服务充当主管。假设您需要检索项目,但必须在将项目返回到客户端之前处理该项目,并且该处理可能需要在其他项目中找到的信息。因此,服务将使用存储库和Finder检索所有适当的项目,然后它将要处理的项目发送到封装必要处理逻辑的对象,最后它将返回客户端请求的项目。有时,不需要处理,也不需要额外的检索,在这种情况下,您不需要提供服务。您可以让客户直接调用存储库和Finder。这是Onion和分层架构的一个不同之处,在洋葱中,更多外部的东西可以更多地访问内部的所有内容,而不仅仅是之前的层。

存储库的角色是加载正确构造它返回的项所需的完整层次结构。因此,如果您的存储库返回的项目具有另一种类型的项目的列表,那么它应该已经解决了这个问题。就个人而言,我喜欢设计我的对象,以便它们不包含对其他项的引用,因为它使存储库更复杂。我更喜欢让我的对象保留其他项的Id,这样如果客户端确实需要其他项,他可以使用给定Id的适当存储库再次查询它。这会使存储库返回的所有项目变得扁平化,但如果需要,仍然可以创建层次结构。

如果您真的觉得有必要,可以在存储库中添加约束机制,以便您可以准确指定所需项目的哪个字段。假设你有一个人,只关心他的名字,你可以做Get(id,name),而且Repository不会费心去获取Person的每个字段,只有它的名字字段。尽管如此,这会给存储库增加相当大的复杂性。使用分层对象执行此操作甚至更复杂,尤其是如果要限制字段字段内的字段。所以我不推荐它。对我来说,唯一的理由就是性能至关重要,而且无法通过其他方式来改善性能。

答案 3 :(得分:6)

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

在示例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;
    }

}

让我们尝试另一种方法,即工作单元,我将使用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;
    }
}

所以这里我们使用'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;
    }
}

那么所有产品和内部类别在哪里?

让我们看一下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命中,因为Find,我们正在获取所有类别字段。此外,还需要启用延迟加载 。不是真正的解决方案吗?

为了改进这一点,我们可以更改查找位置...但是这会将SingleFind委托给ViewModel,它也会返回IQueryable<Category>,我们知道它应该只是一个。

记得我说过“我还在苦苦挣扎?”这主要是为什么。要解决这个问题,我们应该从服务中返回确切需要的数据(也就是说......你知道它......是的!ViewModel)。

所以让我们回到我们的控制器:

public IHttpActionResult Category(int id) {
    var model = service.GetProductCategoryViewModel(id);
    if (category == null) return NotFound(); //
    return View(model);
}

GetProductCategoryViewModel方法中,我们可以调用返回不同部分的私有方法,并将它们组装为ViewModel。

这很糟糕,现在我的服务知道了viewmodels ......让我们解决这个问题。

我们创建一个接口,这个接口是这个方法将返回的实际合约。

ICategoryWithProductsAndChildsIds // quite verbose, i know.

很好,现在我们只需要将我们的ViewModel声明为

public class CategoryViewModel : ICategoryWithProductsAndChildsIds 

以我们想要的方式实现它。

界面看起来有太多东西,当然可以使用ICategoryBasicIProductsIChilds或任何您想要命名的内容进行拆分。

因此,当我们实现另一个viewModel时,我们可以选择只执行IProducts。 我们可以让我们的服务具有方法(私有或非私有)来检索这些合同,并粘合服务层中的各个部分。 (说起来容易做起来难免)

当我进入一个完全正常工作的代码时,我可能会创建一个博客文章或一个github repo,但是现在,我还没有它,所以这一切都是现在。

答案 4 :(得分:4)

在Domain Driven Design中,存储库负责检索整个Aggregate。

答案 5 :(得分:0)

Services in Domain-Driven Design (DDD)上查看Lev Gorodinski服务存储库之间的关系,并着重介绍{{3} }样式:

应用程序服务具有重要且与众不同的作用-它 提供了用于执行域逻辑的托管环境。如 这样,注入各种网关(例如 外部服务的存储库或包装器。

一项服务 Application Core 的一部分,可以看到Vaughn Vernon或Chris Richardson第148页的微服务模式在以下情况下的图形描述: Hexagonal Architecture

另一方面,存储库实现只是一个适配器,可以对应用程序的进行翻译 >消息到存储系统(可以是任何东西:数据库,文件等)。另请参见Onion Architecture接口适配器部分(谈论接口适配器层):

如果数据库是SQL数据库,则所有SQL应该是 仅限此层...

Hexagonal Architecture部分的“ empository adapter”中搜索:阶段3:(FIT或UI)App模拟数据库谈论 repository 实现,被视为< em>适配器。

另一方面,存储库 interface Application Core 的一部分:

围绕领域模型的第一层通常是我们在 查找提供对象保存和检索行为的接口, 称为存储库接口。

根据Eric Evans域驱动设计:

当域中的重要过程或转换不是 ENTITY或VALUE OBJECT的自然责任,请添加操作 模型作为独立接口声明为SERVICE。定义 根据模型的语言进行交互,并确保 操作名称是UBIQUITOUS语言的一部分。进行服务 无状态的。

因此,服务只是包含业务逻辑的东西。

我的结论是:服务是业务逻辑的构建块; 存储库只是一个适配器,可为存储系统提供业务逻辑的访问权限。一个人可以模拟适配器以单独测试业务逻辑,或者可以创建其他存储库适配器以为 application 提供更多存储选项(例如文件适配器,no-sql数据库)适配器等)。

关于GetAllProductsByCategoryIdGetAllXXXBySomeCriteriaYYY

存储库方法应表示其意图,因此它们的命名可能非常具有表现力;尽管这些方法仅包含与存储系统进行通信的逻辑,但它们很好。基于方法命名可能非常有意义的事实,某些框架(例如Spring Data)能够创建/生成这些方法的实现,并且在那里没有错/很奇怪。另一方面,人们可能想以各种方式优化/减少存储库方法的数量,例如让它们接受复杂的条件对象或条件列表,而不是为这些条件的每种组合都使用一种方法,但这不会改变存储库(实现)的角色,它仍然是适配器

关于“加载整个层次结构是存储库的角色吗?”

现在,存储库(实现)的角色应该很清楚:传达用于存储系统的应用程序的消息/请求。消息/请求可以是例如只要可以翻译“给我一个由123标识的人”或“给我一个符合某些条件的人员列表”或“给我一个完整的层次结构符合某些条件” > <存储> 。例如,某些存储系统无法提供/返回层次结构对象,因此应该创建一个 service 来构造层次结构对象。其他存储系统能够提供/返回层次结构对象,但是具有特定于其API /驱动程序的类型,因此存储库(实现)应将其转换为DTO,服务中的服务可能会进一步对其进行处理。以便构造业务层次结构对象。请记住:存储库(实现)只是一个适配器,应该可以对其进行模拟(出于测试目的)。

旁注

还请注意,可能会有更多类型的服务:应用程序服务域服务-请参见Onion Architecture上的描述,但在{{3 }} Hexagonal Architecture之前,应检查以下内容:

域服务和应用程序服务之间的区别 很微妙但很关键

答案 6 :(得分:0)

洋葱和六边形架构的目的是反转域->数据访问的依赖关系。
而不是拥有 UI->api->domain->data-access,
你会有类似 UI->api->domain**<-**data-access
的东西 为了使您最重要的资产,域逻辑处于中心位置并且没有外部依赖。 通常通过将 Repository 拆分为 Interface/Implementation 并将接口与业务逻辑放在一起。

现在是服务,还有不止一种类型的服务:

  • 应用服务:您的控制器和视图模型,它们是 UI 和显示的外部关注点,而不是域的一部分
  • 域服务:提供域逻辑。在这种情况下,如果您在应用程序服务中拥有的逻辑开始执行更多的表示职责。你应该看看提取到域服务
  • 基础设施服务:与存储库一样,在域内有一个接口,在外层有一个实现

@Bart Calixto,您可以查看 CQRS,当您尝试使用为域逻辑设计的存储库时,构建视图模型太复杂了。 您可以为 ViewModel 重写另一个存储库,例如使用 SQL 连接,并且它不必在域中