ApplicationService需要遵循以下原则:

领域驱动设计(DDD)作为一种软件开发方法,可以帮助我们设计出高质量的、能够准确表达业务意图的软件模型。
你应该期待从DDD中得到什么呢?

首先,DDD不应该是一个仪式性的过程,更不应该成为你项目进度的阻碍。
其次,DDD同时提供了战略上和战术上的建模工具来帮助我们设计高质量的软件模型。

需要大家注意的是,DDD不是关于技术的,而是关于讨论、聆听、理解、发现和业务价值的,而这些都是为了将知识集中起来,构建集中化的业务知识体系。
在实践DDD的过程中,你最好将那些不怎么使用技术语言的人(业务领域专家)加进自己的团队,此时你得仔细地聆听他们,还应该尊重他们的观点,并且相信他们比你了解得更多。
谁才是领域专家?

领域专家并不是一个职位,他可以是精通业务的任何人——销售、产品经理、软件开发人员…

什么是领域模型?

领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现(对象=数据+行为),并且表达了准确的业务含义。

作者:爪哇缪斯
链接:https://juejin.cn/post/7117076459889360933
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

ApplicationService需要遵循以下原则:_第1张图片

向内依赖规则

整洁架构图是一个同心圆。它的依赖规则是「依赖方向朝内,每个环可以依赖它本身这一层及其所有内部的层,但不能依赖它外部的层」。就像图中黑色的水平箭头所示的那样。
虽然图中只有四层,但它可以不止四层,可以继续细分或者向外扩展,只要满足「向内依赖规则」就行。

  • Entities
    对应DDD的话,这一层是用来放实体、值对象、聚合等领域模型的。业务逻辑都应该尽量内聚在这一层,这一层是最纯净的,不需要依赖任何其它东西。
  • Use Cases

翻译过来是“用例层”,用于协调进出Entities层的数据流,通过调用和编排领域模型来实现用例。在DDD中,这一层通常是Application Service层,是很薄的一层,只用来做一些比较简单的事情。

  • Interface Adapters

这一层叫“接口适配层”,它其实是主要用来与外部进行适配的。比如Web请求进来的Controllers(写)和Presenters(读)。这一层会将User Cases或Entities层需要的数据结构与外层的数据结构做一个转换。比如操作数据库、调用第三方接口等。

  • Frameworks and Drivers

这一层主要是框架和驱动层,比如数据库驱动、WEB框架、UI等,日常编码中很少会在这一层编写代码。
跨越边界和依赖反转
就如同图中的右下角所示。我们的业务流程通常是先从controller进来,调用Use Cases层。但有时候Use Cases层需要调用presenter。如果直接调用,就破坏了“向内依赖规则”,这个时候可以用“依赖反转”来做。
ApplicationService需要遵循以下原则:_第2张图片
依赖反转
简单来说,我们在Use Cases层定义两种抽象类或者接口:Use Case Input Port, 或者Use Case Output Port。Use Case Interactor去实现了Use Case Input Port,而Presenter去实现了Use Case Output Port。
这样Use Cases层就不用依赖外部的层了。同样的道理,也可用于对数据库、第三方接口等的交互场景。

DDD与整洁架构

ApplicationService需要遵循以下原则:_第3张图片
ApplicationService需要遵循以下原则:_第4张图片
可以看到,处在最中间的是领域模型,然后是领域服务。这两环共同构成了领域层。更外面一层是应用层,应用层也包含了两个环,里面是App Services,外面是C/Q处理器、事件监听器等等。
然后再往外,是大红色轮廓包起来的Application Core。这一层定义了很多接口(也可以说是端口),比如持久化、第三方服务、搜索、CQ总线、事件总线等等。当然,也接收处理命令和查询。
思考,我曾遇到的问题
在之前的DDD项目上,我曾经遇到过一些战术模式上的问题,现在回过头去思考一下。

究竟什么时候需要领域服务?

可以一句话总结:当只使用领域模型做不到的时候。那两者都是在领域层,大家都不依赖外面的东西,什么情况下领域模型做不到,领域服务就做得到?
一个很常见的场景是创建模型的时候有业务逻辑。虽然创建模型通常是放在Factory里面,但Factory里面并不适合放业务逻辑。而这个时候领域模型还没有创建,自然就只能放在领域服务里面了。
当然了,DDD最佳实践是希望能够尽量消灭领域服务层,全部内聚在模型层,只是很难达到这种完美的情况。
在应用层需要查询其它数据怎么办?
有时候我们可能不止是需要当前这个聚合根的数据,可能还需要其它的数据。这个读取操作当然不可能放到领域层去做,通常把它放在应用层。但应用层通常是一个聚合根对应一个ApplicationService,正常的流程是调用Repository接口获取一个领域模型对象,然后对它进行操作,再保存回数据库。
那如果需要获取其它数据怎么办呢?尤其是可能与当前领域模型无关的数据,比如“最近评论时间”。
我们团队之前的解决方案是用了一个Query,Query的职责是去数据库查询,它可以查询部分字段。但为了防止它被滥用,团队规定它只能用于repsenter或者applicationService中“为了写的读”。
回过头去看整洁架构,这里的Query其实就是User Case Output Port。不过当时我们团队的Repository和Query都没有做抽象处理,也就是说并没有依赖反转,所以ApplicationService层依赖了它们,这与整洁架构的“单向依赖”不太相符,这里其实用上依赖反转会好一点。
领域事件究竟该在什么时候发送和接收?
这个也是一个比较有争议的点。首先看领域事件在什么时候创建,有人认为应该在领域层创建,有人认为应该在应用层创建。个人认为在领域层创建比较好,因为创建领域事件其实也算是一种业务逻辑,并且只是创建一个领域事件的话,不会依赖任何外部的东西,放在领域层没有什么问题。
那什么时候发送领域事件呢?按照整洁架构的规则,不应该在领域层发送,因为事件总线(或者事件发送器的实现)在最外层,如果在领域层发送,虽然有依赖倒置,但感觉也跨越太多层次了,不是一个好的实践。
那在应用层发送事件是一个比较好的方案。我们团队之前的方案比较奇葩,是在Repository的实现里面发送的。有点忘记当时为啥这样做了,不过现在看来,应该在应用层去做更合适一些。
事件接收的话,就像上面的图中所示的一样,放在应用层比较好(但在App Services外)。然后可以通过ApplicationService去完成业务逻辑。

跨聚合根的事务问题?

这个其实很难保证强一致的事务了,因为跨聚合根应该使用事件通信,但事件的实现方式有多种,如果是异步,那就保证不了强一致的事务。只能用一些技术手段去尽量保证最终一致。

怎样保证架构不被腐化?

根据之前的实践经验来看,代码架构是有可能随着时间腐化的。比如我前面提到的Query就是一个例子,得通过一些团队间的“共识”来保证它不被滥用。我们不能保证代码完全不被腐化,但是可以通过一些手段去保证依赖的层次不被腐化。
比较推荐的是使用maven/gradle的模块化,因为模块之间是有依赖关系的,只要我们不去改依赖的配置,就永远是单向依赖的。具体来说,我们可以把整洁架构上面的层级分成一个个模块,然后在配置文件里面定义它们的依赖关系。比如应用层模块,依赖领域层模块;接口和适配层模块依赖应用层模块和领域层模块。

永远的难题:定义合适的聚合根

最后谈一个比较难的问题,就是找聚合根。其实大家可能觉得,定义一个聚合根应该很简单,根据业务来就是了,比如用户、订单、商品、库存等等。但有时候我们很容易把聚合根定义得很大,因为无论聚合根多大,它都能够很好地解释。有个建模界的笑话是:我定义了一个“宇宙类”,它可以包含所有模型。
聚合根太大可能会有问题,比如代码过多、测试用例过多、性能不好等等。很有可能做着做着一个聚合根就膨胀了,这个时候你们尝试去拆分它,会发现并非像最开始现象的那样不能拆分。只是要找到合适的“借口”,要拆得有理有据。但总而言之,这是一件很难的事情。

作者:编了个程
链接:https://juejin.cn/post/6887366391675289608
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
ApplicationService需要遵循以下原则:_第5张图片
ApplicationService需要遵循以下原则:
业务方法与业务用例一一对应:前面已经讲到,不再赘述。
业务方法与事务一一对应:也即每一个业务方法均构成了独立的事务边界,在本例中,OrderApplicationService.changeProductCount()方法标记有Spring的@Transactional注解,表示整个方法被封装到了一个事务中。
本身不应该包含业务逻辑:业务逻辑应该放在领域模型中实现,更准确的说是放在聚合根中实现,在本例中,order.changeProductCount()方法才是真正实现业务逻辑的地方,而ApplicationService只是作为代理调用order.changeProductCount()方法,因此,ApplicationService应该是很薄的一层。
与UI或通信协议无关:ApplicationService的定位并不是整个软件系统的门面,而是领域模型的门面,这意味着ApplicationService不应该处理诸如UI交互或者通信协议之类的技术细节。在本例中,Controller作为ApplicationService的调用者负责处理通信协议(HTTP)以及与客户端的直接交互。这种处理方式使得ApplicationService具有普适性,也即无论最终的调用方是HTTP的客户端,还是RPC的客户端,甚至一个Main函数,最终都统一通过ApplicationService才能访问到领域模型。
接受原始数据类型:ApplicationService作为领域模型的调用方,领域模型的实现细节对其来说应该是个黑盒子,因此ApplicationService不应该引用领域模型中的对象。此外,ApplicationService接受的请求对象中的数据仅仅用于描述本次业务请求本身,在能够满足业务需求的条件下应该尽量的简单。因此,ApplicationService通常处理一些比较原始的数据类型。在本例中,OrderApplicationService所接受的Order ID是Java原始的String类型,在调用领域模型中的Repository时,才被封装为OrderId对象。

ApplicationService需要遵循以下原则:_第6张图片

ApplicationService需要遵循以下原则:_第7张图片

ACL防腐层的简单原理如下:

对于依赖的外部对象,我们抽取出所需要的字段,生成一个内部所需的VO或DTO类。
构建一个新的Facade,在Facade中封装调用链路,将外部类转化为内部类。
针对外部系统调用,同样的用Facade方法封装外部调用链路。
无防腐层的情况:

ApplicationService需要遵循以下原则:_第8张图片

来说一下,你们是怎么解决分布式场景下的事务问题?

应用层连接用户接口层和领域层,主要协调领域层,面向用例和业务流程,协调多个聚合完成服务的组合和编排,在这一层不实现任何业务逻辑,只是很薄的一层。
应用层的核心类:

ApplicationService应用服务:最核心的类,负责业务流程的编排,但本身不负责任何业务逻辑。有时会简写为“AppService”。一个ApplicationService类是一个完整的业务流程,其中每个方法负责处理一个Use Case,比如订单的各种用例(下单、支付成功、发货、收货、查询)。
DTO Assembler:负责将内部领域模型转化为可对外的DTO。
返回的DTO:作为ApplicationService的出参。
Command指令:指调用方明确想让系统操作的指令,其预期是对一个系统有影响,也就是写操作。通常来讲指令需要有一个明确的返回值(如同步的操作结果,或异步的指令已经被接受)。
Query查询:指调用方明确想查询的东西,包括查询参数、过滤、分页等条件,其预期是对一个系统的数据完全不影响的,也就是只读操作。
Event事件:指一件已经发生过的既有事实,需要系统根据这个事实作出改变或者响应的,通常事件处理都会有一定的写操作。事件处理器不会有返回值。这里需要注意一下的是,Application层的Event概念和Domain层的DomainEvent是类似的概念,但不一定是同一回事,这里的Event更多是外部一种通知机制而已。

ApplicationService的接口入参只能是一个Command、Query或Event对象,CQE对象需要能代表当前方法的语意。这样的好处是提升了接口的稳定性、降低低级的重复,并且让接口入参更加语意化。

领域层是领域模型的核心,主要实现领域模型的核心业务逻辑,体现领域模型的业务能力。领域层关注实现领域对象的充血模型和聚合本身的原子业务逻辑,至于用户操作和业务流程,则交给应用层去编排。这样设计可以保证领域模型不容易受外部需求变化的影响,保证领域模型的稳定。
领域层的核心类:

实体类(Entity):大多数DDD架构的核心都是实体类,实体类包含了一个领域里的状态、以及对状态的直接操作。Entity最重要的设计原则是保证实体的不变性(Invariants),也就是说要确保无论外部怎么操作,一个实体内部的属性都不能出现相互冲突,状态不一致的情况。
值对象(VO):通常是用来度量和描述事物。我们可以非常容易的对其进行创建,测试,使用,优化和维护,所以在建模时,我们尽量采用值对象来建模。
聚合根(Aggr):聚合是由业务和逻辑紧密关联的实体和值对象组合而成的。聚合是数据修改和持久化的基本单元。每个聚合都有一个根实体,叫做聚合根,外界只能通过聚合根跟聚合通信。聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致的问题。
领域服务(DomainService):当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务了。可以使用领域服务的地方,过度使用领域服务将导致贫血领域模型。执行一个显著的业务操作过程;对领域对象进行转换;已多个领域对象作为输入进行计算,结果产生一个值对象。
仓储层接口(Repository):把我们要的数据当做一个集合放在仓储里面,想要的时候直接获取。仓储作为领域层和基础结构层的连接组件,使得领域层不必过多的关注存储细节。在设计时,将仓储接口放在领域层,而将仓储的具体实现放在基础结构层,领域层通过接口访问数据存储,而不必过多的关注仓储存储数据的细节,这样使得领域层将更多的关注点放在领域逻辑上面。
工厂(Factory):对于实体等对象构造比较麻烦的,可以借助工厂进行构造。

作者:狼爷
链接:https://juejin.cn/post/7040826223500967972
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(DDD,java,开发语言)