【问题标题】:Onion Architecture - Repository Vs Service?洋葱架构 - 存储库与服务?
【发布时间】:2012-09-01 06:42:30
【问题描述】:

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

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

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

【问题讨论】:

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


    【解决方案1】:

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

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

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

    对于第二个问题:如果我在代码中看到 ProductRepository,我希望它会返回一个 Product 列表。我还希望每个 Product 实例都是完整的。例如,如果 Product 引用了 ProductDetail 对象,我希望 Product.getDetail() 返回一个 ProductDetail 实例而不是 null。可能是存储库加载 ProductDetail 和 Product 的实现,可能是 getDetail() 方法在运行中调用 ProductDetailRepository。作为存储库的用户,我真的不在乎。当我调用getDetail() 时,产品也可能只返回ProductDetail id。从存储库的合同的角度来看,这是完美的。然而,它使我的客户端代码复杂化,并迫使我自己打电话给ProductDetailRepository

    顺便说一句,我过去见过许多只包装存储库类的服务类。我认为这是一种反模式。最好让服务的调用者直接使用存储库。

    【讨论】:

    • 你说的不建议添加GetAllProductsByCategoryId或者GetProductByName这样的方法。如果不建议在存储库中编写这些方法,那么最好的地方是什么?是服务层吗?
    • 使用 GetProductByCategoryId 等预定义方法与在存储库上使用规范查询是有争议的。有人说使用规范会模糊合同并增加复杂性。使用方法,您只需查看存储库,就知道支持哪些用例。您可以更好地控制客户可以做什么或不可以做什么。有了规范,您就可以接受可能不需要或不需要的用例。一旦您向客户提供可能的查询,您就永远无法收回它,并且必须始终确保您的存储库支持它。
    • + 1 到 nwang0。任何自定义业务逻辑都应该进入您的业务层。理想情况下,整个数据访问层可以自动生成(使用类似sswdataonion.com 的东西)。您还可以将所有存储库创建为部分类,以便其中大部分是自动生成的,并且可以添加任何自定义逻辑。
    • @nwang0 > 你的意思是像 lambda 表达式形式的谓词?
    【解决方案2】:

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

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

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

    例如OrderOrderLines

    OrderLines 没有理由在没有其父 Order 的情况下存在,它们也不能属于任何其他 Order。在这种情况下,Order 和 OrderLines 可能是一个聚合,而 Order 是聚合根

    业务逻辑应该在域实体中,而不是在存储库层中,应用程序逻辑应该像你提到的那样在服务层中,这里的服务在存储库之间扮演协调者的角色。

    【讨论】:

    • 嗨@Coung Le,我也在学习洋葱架构,我也很困惑在哪里实现像GetProductsByCategoryId这样的方法。目前,我正在服务层中实现这些类型的方法,按照 nwang 的建议,使用 Ninject 将这些方法注入到控制器中。请告诉我我走对了吗?
    【解决方案3】:

    虽然我仍在为此苦苦挣扎,但我想发布作为答案,但我也接受(并希望)对此的反馈。

    在示例中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 和存储库,因此无需创建这些。

    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 hits,并且由于 Find 的原因,我们正在获取所有类别字段。此外,延迟加载必须启用。不是真正的解决方案吗?

    为了改善这一点,我们可以将 find 更改为 where... 但这会将 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。

    这很糟糕,现在我的服务知道视图模型...让我们解决这个问题。

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

    ICategoryWithProductsAndChildsIds // quite verbose, i know.
    

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

    public class CategoryViewModel : ICategoryWithProductsAndChildsIds 
    

    并按照我们想要的方式实现它。

    界面看起来东西太多了,当然可以拆分成ICategoryBasicIProductsIChilds,或者随便你怎么命名。

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

    当我编写完整的工作代码时,我可能会创建一篇博文或一个 github 存储库,但目前我还没有,所以暂时就这些了。

    【讨论】:

      【解决方案4】:

      我相信存储库应该只用于 CRUD 操作。

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

      因此,IRepository 将具有:Add、Remove、Update、Get、GetAll 以及可能每个采用列表的版本,即 AddMany、RemoveMany 等。

      为了执行搜索检索操作,您应该有第二个接口,例如 IFinder。您可以使用规范,因此 IFinder 可以有一个采用标准的 Find(criteria) 方法。或者您可以使用 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 方法,以及实现 IFinder 的 IMyObjectFinder,但还可以添加自定义方法,例如 FindByName。

      该服务充当监督者。假设您需要检索一个项目,但还必须在将该项目返回给客户端之前对其进行处理,并且该处理可能需要在其他项目中找到的信息。因此,服务将使用存储库和查找器检索所有适当的项目,然后将要处理的项目发送到封装必要处理逻辑的对象,最后返回客户端请求的项目。有时,不需要处理和额外的检索,在这种情况下,您不需要服务。您可以让客户端直接调用存储库和查找器。这是 Onion 和分层架构的一个区别,在 Onion 中,更外部的所有内容都可以访问更内部的所有内容,而不仅仅是之前的层。

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

      如果您真的觉得有必要,您可以在您的存储库中添加一个限制机制,以便您可以准确地指定您需要的项目的哪个字段。假设您有一个 Person,并且只关心他的名字,您可以执行 Get(id, name) 并且存储库不会费心获取 Person 的每个字段,只关心它的 name 字段。但是,这样做会给存储库增加相当大的复杂性。使用分层对象执行此操作更加复杂,特别是如果您想限制字段字段内的字段。所以我真的不推荐它。对我来说,这样做的唯一充分理由是性能至关重要,并且无法采取其他任何措施来提高性能。

      【讨论】:

      • Finder 是直接访问数据存储还是使用存储库获取所有数据然后过滤?
      • @pkidza 两者都有效,但直接访问数据存储通常会给您带来更好的性能。为简单起见,您可以先在后台使用 Repo 的 getAll 并在代码中进行过滤。但我怀疑在规模上,这可能需要重构以提高性能。如果您使用基于标准的 Finder,请小心。您可能可以在代码过滤中使用复杂的标准语法,但是在您需要性能的那一天将其转换为您的数据存储查询语言可能并不容易。
      【解决方案5】:

      在领域驱动设计中,存储库负责检索整个聚合。

      【讨论】:

      • 这个问题恐怕我没听懂。
      • 如果存储库正在加载整个聚合,那么服务层的目的是什么?在我看来,该服务负责从不同的存储库中获取所有数据以进行聚合。这才是真正的生意。存储库只关心单一类型实体的数据访问(添加、获取、更新)。正如你所说,一些存储库做得更多。那是我的问题;我不明白为什么和什么时候
      • 存储库负责加载聚合。如果流程需要,服务可以使用多个存储库来检索多个不同的聚合。请重新阅读“领域驱动设计”中的各个章节,掌握概念需要一些时间。
      • @Cyber​​maxs Repository 实现了解如何持久化实体(通常在数据库中)的细节。它们驻留在基础设施/持久层中。域服务不处理数据库。
      • @DennisTraub 实现 GetProductsByCategoryId、Repositories 或 Service 等方法的最佳位置是什么?我也是洋葱架构的新手,我正在服务层中实现这些类型的方法。请指导我,我在正确的轨道上吗?
      【解决方案6】:

      Lev GorodinskiServices in Domain-Driven Design (DDD) 上查看服务存储库 之间的关系,其中Vaughn Vernon 的更新强调Hexagonal Architecture 风格:

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

      服务Application Core 的一部分,正如 Onion Architecture 或 Chris Richardson 第 148 页在 @987654326 上下文中以图形方式描述的微服务模式@。

      另一方面,存储库实现只是一个适配器,它能够翻译应用程序的 到存储系统的消息(可以是任何东西:数据库、文件等)。另请参阅The Clean ArchitectureInterface Adapters 部分(谈论 Interface Adapters 层):

      如果数据库是SQL数据库,那么所有的SQL都应该是 仅限于这一层...

      Hexagonal Architecture 部分搜索“存储库适配器”第 3 阶段:(FIT 或 UI)应用程序模拟数据库谈论将 存储库 实现视为 适配器

      另一方面,存储库接口应用程序核心的一部分:

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

      • Hexagonal Architecture 的上下文中,另请参阅 Chris Richardson 的 Microservices Patterns 第 159 页

      根据 Eric Evans 领域驱动设计:

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

      所以,服务只是包含业务逻辑的东西。

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

      关于GetAllProductsByCategoryIdGetAllXXXBySomeCriteriaYYY

      存储库方法应该表明它们的意图,这样它们的命名就可以非常有表现力;虽然这些方法只包含与存储系统通信的逻辑,但它们很好。基于方法命名可能非常有意义的事实,一些框架(例如 Spring Data)能够为这些方法创建/生成实现,并且没有任何错误/奇怪的地方。另一方面,人们可能希望以各种方式优化/减少存储库方法的数量,例如让它们接受复杂的标准对象或标准列表,而不是为这些标准的每个组合提供一个方法,但这不会改变仍然是适配器的存储库(实现)的角色。

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

      现在存储库(实现)的作用应该很清楚了:传送应用程序的消息/请求,用于存储系统。消息/请求可以是例如只要消息/请求可以翻译 到存储系统。例如,一些存储系统无法提供/返回一个层次结构对象,因此应该创建一个服务来构造层次结构对象;其他存储系统能够提供/返回一个层次结构对象,但其类型特定于其 API/驱动程序,因此存储库(实现)应将其转换为 DTO,该 DTO 可能由服务进一步处理为了构建业务层次结构对象。请记住:repository(实现)只是一个 适配器,应该可以模拟(用于测试目的)。

      旁注

      还请注意,可能有更多类型的服务:应用程序服务域服务 - 请参阅Onion Architecture 中的描述,但Services in Domain-Driven Design (DDD) 中更好地解释了@ 987654333@ 应该检查的地方:

      领域服务和应用服务的区别 微妙但关键

      【讨论】:

      • 存储层是否可以返回一个布尔值来指示具有特定条件的对象是否存在?
      • 是的,您可以从存储库中提取任何内容。我会说原语及其简单的包装器总是很好。想象一下,您只有没有已知业务的存储库接口:存储库方法的结果是否仍然有意义?如果是,则该方法具有有效的返回类型。
      【解决方案7】:

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

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

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

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

      【讨论】:

        猜你喜欢
        • 2012-05-13
        • 2023-04-02
        • 2013-06-30
        • 2014-06-22
        • 2015-03-17
        • 2011-10-09
        • 2016-08-12
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多