【系统设计】大神三分钟搞懂领域驱动设计(二)

简介: 【系统设计】大神三分钟搞懂领域驱动设计

存储库,工厂和服务(Repositories, Factories and Services)

在企业应用程序中,实体通常是持久的,其值表示这些实体的状态。但是,我们如何从持久性存储中获取实体呢?

存储库是持久性存储的抽象,返回实体 - 或者更确切地说是聚合根 - 满足某些标准。例如,客户存储库将返回Customer聚合根实体,订单存储库将返回Orders(及其OrderItems)。通常,每个聚合根有一个存储库。

因为我们通常希望支持持久性存储的多个实现,所以存储库通常由具有不同持久性存储实现的不同实现的接口(例如,CustomerRepository)组成(例如,CustomerRepositoryHibernate或CustomerRepositoryInMemory)。由于此接口返回实体(域层的一部分),因此接口本身也是域层的一部分。接口的实现(与一些特定的持久性实现耦合)是基础结构层的一部分。

我们搜索的标准通常隐含在名为的方法名称中。因此,CustomerRepository可能会提供findByLastName(String)方法来返回具有指定姓氏的Customer实体。或者我们可以让OrderRepository返回Orders,findByOrderNum(OrderNum)返回与OrderNum匹配的Order(请注意,这里使用值类型!)。

更复杂的设计将标准包装到查询或规范中,类似于findBy(Query <T>),其中Query包含描述标准的抽象语法树。然后,不同的实现解包查询以确定如何以他们自己的特定方式定位满足条件的实体。

也就是说,如果你是.NET开发人员,那么值得一提的是LINQ [8]。因为LINQ本身是可插拔的,所以我们通常可以使用LINQ编写存储库的单个实现。然后变化的不是存储库实现,而是我们配置LINQ以获取其数据源的方式(例如,针对Entity Framework或针对内存中的对象库)。

每个聚合根使用特定存储库接口的变体是使用通用存储库,例如Repository <Customer>。这提供了一组通用方法,例如每个实体的findById(int)。当使用Query <T>(例如Query <Customer>)对象指定条件时,这很有效。对于Java平台,还有一些框架,例如Hades [9],允许混合和匹配方法(从通用实现开始,然后在需要时添加自定义接口)。

存储库不是从持久层引入对象的唯一方法。如果使用对象关系映射(ORM)工具(如Hibernate),我们可以在实体之间导航引用,允许我们透明地遍历图形。根据经验,对其他实体的聚合根的引用应该是延迟加载的,而聚合中的聚合实体应该被急切加载。但与ORM一样,期望进行一些调整,以便为最关键的用例获得合适的性能特征。

在大多数设计中,存储库还用于保存新实例,以及更新或删除现有实例。如果底层持久性技术支持它,那么它们很可能存在于通用存储库中,但是从方法签名的角度来看,没有什么可以区分保存新客户和保存新订单。

最后一点......直接创建新的聚合根很少见。相反,它们倾向于由其他聚合根创建。订单就是一个很好的例子:它可能是通过客户调用一个动作来创建的。

这整齐地带给我们:

工厂

如果我们要求Order创建一个OrderItem,那么(因为毕竟OrderItem是其聚合的一部分),Order知道要实例化的具体OrderItem类是合理的。实际上,实体知道它需要实例化的同一模块(命名空间或包)中的任何实体的具体类是合理的。

假设客户使用Customer的placeOrder操作创建订单(参见图6)。如果客户知道具体的订单类,则意味着客户模块依赖于订单模块。如果订单具有对客户的反向引用,那么我们将在两个模块之间获得循环依赖。

Figure 6: Customers and Orders (cyclic dependencie

如前所述,我们可以使用依赖性反转原则来解决这类问题:从订单中删除依赖关系 - >客户模块我们将引入OrderOwner接口,使Order引用为OrderOwner,并使Customer实现OrderOwner(参见图7))。

Figure 7: Customers and Orders (customer depends o

那么另一种方式呢:如果我们想要订单 - >客户? 在这种情况下,需要在客户模块中有一个表示Order的接口(这是Customer的placeOrder操作的返回类型)。 然后,订单模块将提供订单的实现。 由于客户不能依赖订单,因此必须定义OrderFactory接口。 然后,订单模块依次提供OrderFactory的实现(参见图8)。


可能还有相应的存储库接口。例如,如果客户可能有数千个订单,那么我们可能会删除其订单集合。相反,客户将使用OrderRepository根据需要定位其订单(的一部分)。或者(如某些人所愿),您可以通过将对存储库的调用移动到应用程序体系结构的更高层(例如域服务或应用程序服务)来避免从实体到存储库的显式依赖性。

实际上,服务是我们需要探索的下一个话题。

域服务,基础结构服务和应用程序服务(Domain services, Infrastructure services and Application services)

域服务(domain service)是在域层内定义的域服务,但实现可以是基础结构层的一部分。存储库是域服务,其实现确实在基础结构层中,而工厂也是域服务,其实现通常在域层内。特别是在适当的模块中定义了存储库和工厂:CustomerRepository位于客户模块中,依此类推。

更一般地说,域服务是任何不容易在实体中生存的业务逻辑。埃文斯建议在两个银行账户之间进行转账服务,但我不确定这是最好的例子(我会将转账本身建模为一个实体)。但另一种域服务是一种充当其他有界上下文的代理。例如,我们可能希望与暴露开放主机服务的General Ledger系统集成。我们可以定义一个公开我们需要的功能的服务,以便我们的应用程序可以将条目发布到总帐。这些服务有时会定义自己的实体,这些实体可能会持久化;这些实体实际上影响了在另一个BC中远程保存的显着信息。

我们还可以获得技术性更强的服务,例如发送电子邮件或SMS文本消息,或将Correspondence实体转换为PDF,或使用条形码标记生成的PDF。接口在域层中定义,但实现在基础架构层中非常明确。因为这些非常技术性服务的接口通常是根据简单的值类型(而不是实体)来定义的,所以我倾向于使用术语基础结构服务(infrastructure service)而不是域服务。但是如果你想成为一个“电子邮件”BC或“SMS”BC的桥梁,你可以想到它们。

虽然域服务既可以调用域实体也可以调用域实体,但应用服务(application service)位于域层之上,因此域层内的实体不能调用,只能反过来调用。换句话说,应用层(我们的分层架构)可以被认为是一组(无状态)应用服务。

如前所述,应用程序服务通常处理交叉和安全等交叉问题。他们还可以通过以下方式与表示层进行调解:解组入站请求;使用域服务(存储库或工厂)获取对与之交互的聚合根的引用;在该聚合根上调用适当的操作;并将结果编组回表示层。

我还应该指出,在某些体系结构中,应用程序服务调用基础结构服务。因此,应用服务可以直接调用PdfGenerationService,传递从实体中提取的信息,而不是实体调用PdfGenerationService将其自身转换为PDF。这不是我的特别偏好,但它是一种常见的设计。我很快就会谈到这一点。

好的,这完成了我们对主要DDD模式的概述。在Evans 500 +页面书中还有更多内容 - 值得一读 - 但我接下来要做的是突出显示人们似乎很难应用DDD的一些领域。

问题和障碍

实施分层架构

这是第一件事:严格执行架构分层可能很困难。特别是,从域层到应用层的业务逻辑渗透可能特别隐蔽。

我已经在这里挑出了Java的EJB2作为罪魁祸首,但是模型 - 视图 - 控制器模式的不良实现也可能导致这种情况发生。控制器(=应用层)会发生什么,承担太多责任,让模型(=域层)变得贫血。事实上,有更新的Web框架(在Java世界中,Wicket [10]是一个崭露头角的例子),出于这种原因明确地避免了MVC模式。

表示层模糊了域层

另一个问题是尝试开发无处不在的语言。领域专家在屏幕方面谈话是很自然的,因为毕竟,这就是他们可以看到的系统。要求他们在屏幕后面查看并在域概念方面表达他们的问题可能非常困难。

表示层本身也可能存在问题,因为自定义表示层可能无法准确反映(可能会扭曲)底层域概念,从而破坏我们无处不在的语言。即使不是这种情况,也只需要将用户界面组合在一起所需的时间。使用敏捷术语,速度降低意味着每次迭代的进度较少,因此对整个域的深入了解较少。

存储库模式的实现

从更技术性的角度来看,新手有时似乎也会混淆将存储库(在域层中)与其实现(在基础架构层中)的接口分离出来。我不确定为什么会这样:毕竟,这是一个非常简单的OO模式。我想这可能是因为埃文斯的书并没有达到这个细节水平,这让一些人变得高高在上。但这也可能是因为替换持久性实现(根据六边形体系结构)的想法并不普遍,导致持久性实现渗透到域层的系统。

服务依赖项的实现

另一个技术问题 - 在DDD从业者之间可能存在分歧 - 就实体与域/基础设施服务(包括存储库和工厂)之间的关系而言。有些人认为实体根本不应该依赖域服务,但如果是这种情况,则外部应用程序服务与域服务交互并将结果传递给域实体。根据我的思维方式,这使我们走向了一个贫血的领域模型。

稍微柔和的观点是实体可以依赖于域服务,但应用程序服务应该根据需要传递它们,例如作为操作的参数。我也不喜欢这个:对我而言,它将实现细节暴露给应用层(“这个实体需要这样一个服务才能完成这个操作”)。但是许多从业者对这种方法感到满意。

我自己的首选方案是使用依赖注入将服务注入实体。实体可以声明它们的依赖关系,然后基础结构层(例如Hibernate,Spring或其他一些框架)可以将服务注入实体:

public class Customer {
 private OrderFactory orderFactory;
 public void setOrderFactory(OrderFactory orderFactory) {
 this.orderFactory = orderFactory;
 }
 public Order placeOrder( … ) {
 Order order = orderFactory.createOrder();
 return order;
 }
 }

一种替代方法是使用服务定位器模式。例如,将所有服务注册到JNDI中,然后每个域对象查找它所需的服务。在我看来,这引入了对运行时环境的依赖。但是,与依赖注入相比,它对实体的内存需求较低,这可能是一个决定性因素。

不合适的模块化

正如我们已经确定的那样,DDD在实体之上区分了几种不同的粒度级别,即聚合,模块和BC。获得正确的模块化水平需要一些练习。正如RDBMS模式可能被非规范化一样,系统也没有模块化(成为泥浆的大球)。但是,过度规范化的RDBMS模式(其中单个实体在多个表上被分解)也可能是有害的,过模块化系统也是如此,因为它变得难以理解系统如何作为整体工作。

我们首先考虑模块和BC。记住,模块类似于Java包或.NET命名空间。我们希望两个模块之间的依赖关系是非循环的,但是如果我们确定(比如说)客户依赖于订单,那么我们不需要做任何额外的事情:客户可以简单地导入Order包/命名空间并使用它接口和类根据需要。

但是,如果我们将客户和订单放入单独的BC中,那么我们还有更多的工作要做,因为我们必须将客户BC中的概念映射到BC订单的概念。在实践中,这还意味着在客户BC中具有订单实体的表示(根据前面给出的总分类帐示例),以及通过消息总线或其他东西实际协作的机制。请记住:拥有两个BC的原因是当有不同的最终用户和/或利益相关者时,我们无法保证不同BC中的相关概念将朝着相同的方向发展。

另一个可能存在混淆的领域是将实体与聚合区分开来。每个聚合都有一个实体作为其聚合根,对于很多很多实体,聚合将只包含这个实体(“琐碎”的情况,正如数学家所说的那样)。但我看到开发人员认为整个世界必须存在于一个聚合中。因此,例如,订单包含引用产品的OrderItems(到目前为止一直很好),因此开发人员得出结论,产品也在聚合中(不!)更糟糕的是,开发人员会观察到客户有订单,所以想想这个意味着我们必须拥有Customer / Order / OrderItem / Product的巨型聚合(不,不,不!)。关键是“客户有订单”并不意味着暗示汇总;客户,订单和产品都是集合的根源。

实际上,一个典型的模块(这是非常粗糙和准备好的)可能包含六个聚合,每个聚合可能包含一个实体和几个实体之间。在这六个中,一个好的数字可能是不可变的“参考数据”类。还要记住,我们模块化的原因是我们可以理解一件事(在一定的粒度级别)。所以要记住,典型的人一次只能保持在5到9个之间[11]。

相关文章
|
9月前
|
监控 前端开发 测试技术
吃透这些软件测试理论知识要点,你就搞懂了软件测试
吃透这些软件测试理论知识要点,你就搞懂了软件测试
327 0
|
9月前
|
供应链 架构师 数据库
架构师带你搞明白微服务进阶场景实战:服务之间的数据依赖问题
数据同步 上面讲解了数据一致性的解决方案,这一篇来讲讲服务之间的数据依赖问题,还是先来说说具体的业务场景。 业务场景:如何解决微服务之间的数据依赖问题 在某个供应链系统中,存在商品、订单、采购这3个服务,它们的主数据部分结构表如下。
架构师带你搞明白微服务进阶场景实战:服务之间的数据依赖问题
|
1月前
|
安全 程序员 数据安全/隐私保护
终于有篇文章把后管权限系统设计讲清楚了
【2月更文挑战第1天】在常用的后台管理系统中,通常都会有权限系统设计,以用于给对应人员分配不同权限,控制其对后管系统中的某些菜单、按钮以及列表数据的可见性。
114 2
终于有篇文章把后管权限系统设计讲清楚了
|
11月前
|
SQL 存储 缓存
系统设计不知怎么入手?一文帮到你
系统设计不知怎么入手?一文帮到你
83 2
|
存储 开发框架 Java
【领域驱动设计】三分钟搞懂领域驱动设计(一)
【领域驱动设计】三分钟搞懂领域驱动设计
|
存储 开发框架 Java
【领域驱动设计】大神三分钟搞懂领域驱动设计(上)
【领域驱动设计】大神三分钟搞懂领域驱动设计
|
人工智能 缓存 运维
系统设计面试的框架
系统设计面试的框架
|
存储 开发框架 数据可视化
【系统设计】大神三分钟搞懂领域驱动设计(三)
【系统设计】大神三分钟搞懂领域驱动设计
【系统设计】大神三分钟搞懂领域驱动设计(三)
|
前端开发 Java 程序员
【系统设计】大神三分钟搞懂领域驱动设计(一)
【系统设计】大神三分钟搞懂领域驱动设计
【系统设计】大神三分钟搞懂领域驱动设计(一)
|
存储 前端开发 数据可视化
【领域驱动设计】三分钟搞懂领域驱动设计(二)
【领域驱动设计】三分钟搞懂领域驱动设计