mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-11-17 06:33:49 +08:00
fix img
This commit is contained in:
@@ -538,7 +538,7 @@ function hide_canvas() {
|
||||
<p>远程服务是为跨进程通信定义的外部服务,本质上属于北向网关,遵循了上下文映射的开放主机服务(Open Host Service,OHS)模式。根据通信协议和消费者的差异,远程服务又分为资源(Resource)服务、供应者(Provider)服务和控制器(Controller)服务。资源与供应者服务的定义受到通信协议的影响,而控制器服务则是因为它主要面向前端视图的消费请求,满足 MVC 模式。</p>
|
||||
<p>应用服务同样可以作为限界上下文的外部服务。由于它是对领域模型的一层包装,故而在概念归属上仍然属于限界上下文中表达领域的组成部分,并不属于提供了技术实现的基础设施层。应用服务不具备跨进程通信的能力,这就决定了它的调用者必须与它处于同一个进程,依赖的方式可以是代码依赖,也可以是二进制依赖,但在运行时,却处于同一个进程的内存空间。</p>
|
||||
<p>显然,远程服务和应用服务的作用各不相同,前者面向进程外通信,后者面向进程内通信。对于进程内的限界上下文协作而言,为避免不必要的网络通信,下游限界上下文应调用上游限界上下文的应用服务,而非远程服务。如果两个限界上下文分别作为独立的微服务,它们之间的协作则通过远程服务来完成:</p>
|
||||
<p><img src="assets/cf7c6f80-1a86-11ea-9149-1fa1a24c0c48" alt="73893538.png" /></p>
|
||||
<p><img src="assets/cf7c6f80-1a86-11ea-9149-1fa1a24c0c48" alt="png" /></p>
|
||||
<p>即便系统采用单体架构,由于大多数限界上下文还需要面对前端视图的调用,为其定义远程服务仍有必要;倘若采用了微服务架构,并不意味着每个限界上下文都是微服务,某些限界上下文会以代码库的方式被微服务重用,故而为微服务上下文保留应用服务亦有必要。更何况,限界上下文的边界总存在不确定性,正所谓“分久必合,合久必分”,限界上下文的边界在进程内外发生调整,亦是常有的事儿,因此有必要在上下文内部同时保留远程服务与应用服务。当然,在一些简单架构下,将应用层和基础设施层中的远程服务合二为一,可以减少不必要的间接层次,算是一种例外的选择。</p>
|
||||
<h4>外部服务接口的定义</h4>
|
||||
<p>当一个限界上下文可能存在两种不同的服务向外部暴露时,面对外部的调用者,究竟该如何设计服务接口呢?遵循面向接口设计的原则,我们需要站在调用者的角度去思考接口的定义。外部服务接口的调用者通常包括:</p>
|
||||
@@ -563,7 +563,7 @@ public class OrderAppService {
|
||||
<p>当应用服务的接口定义为领域模型对象时,应用服务承担的职责更少,因为它无需负责对输入参数与返回值的转换,调用领域服务变得更加惬意。随之而来的问题是这样的接口会泄露位于内部核心的领域模型。它存在一种风险,当下游限界上下文没有通过防腐层(Anticorruption Layer,ACL)调用该接口时,就会产生下游对上游领域模型的依赖,形成遵奉者(Conformist)。</p>
|
||||
<p>即使应用服务无需承担模型对象之间的转换逻辑,并不意味着限界上下文就能免去这一职责,不过是将该职责“转嫁”给了远程服务罢了。由于应用服务接口往往体现了具有业务价值的完整业务用例,细粒度的领域模型对象未必能够满足下游限界上下文调用者的意愿;更何况,模型对象之间的转换总会包含一部分领域逻辑,让位于基础设施层的远程服务来做这件事情,似有职责分配失当之嫌。两相比较,我更倾向于<strong>应用服务接口定义为消息契约对象</strong>,并将领域模型对象与消息契约对象之间的转换“留”在应用层。</p>
|
||||
<p>如 1-18 课《领域驱动分层架构与对象模型》总结的那样,消息契约对象可以分为请求(Request)消息和响应(Response)消息。它们与远程服务对象共同组成了服务对象模型:</p>
|
||||
<p><img src="assets/4ebe0860-1a89-11ea-9f7c-c19aaff64deb" alt="73938529.png" /></p>
|
||||
<p><img src="assets/4ebe0860-1a89-11ea-9f7c-c19aaff64deb" alt="png" /></p>
|
||||
<p>为了表达请求消息的调用行为,我建议以动名词短语结合 Request 后缀的形式定义请求消息对象,除非这些请求消息可以由简单的内建类型来表达。以 Spring Boot 为例,在执行 GET 动作时,如果请求消息通过 @RequestParam 或者 @PathVariable 定义,不妨就直接暴露该参数的类型,否则就应该定义专有的请求对象,如下订单请求的 PlacingOrderRequest。</p>
|
||||
<p>对于响应消息,命令结果往往包含执行成功或失败的标识,如果希望以更丰富的结果对象表达,则可以定义为包含了请求动词的响应对象,如 DeletionResultResponse。视图模型和数据契约分别面向 UI 客户端和非UI客户端,可以根据对应的数据模型进行命名,以订单为例,就可以分别命名为 OrderViewResponse 和 OrderResponse。如果返回的视图模型和数据契约为集合,除非该集合自身也具有业务含义,否则可以直接使用语言提供的集合类型,如 List<OrderResponse>。通常建议返回的视图模型与数据契约对象尽量以扁平的结构返回,若确实需要嵌套,如 Order 嵌套 OrderItem,则内嵌的类型也应定义为对应的响应对象,而非直接使用领域模型对象。</p>
|
||||
<p>若消息契约对象定义在应用层,切忌引入对外部框架的依赖。例如,对于命令请求而言,REST 服务要求返回标准的状态码,一些 REST 框架如 Spring Boot 定义了自己的状态码,如 HttpStatus.NOT_FOUND,这样的状态码就不应该定义在消息契约对象中。可以通过自定义的错误码,或者定义不同类型的 ApplicationException 来传递这些状态信息。远程服务在调用了应用服务之后,可以由其自行处理。</p>
|
||||
@@ -689,7 +689,7 @@ public class OrderAppService {
|
||||
</ul>
|
||||
<p>在作为下游的订单上下文中,除了 InventoryAppServiceClient <strong>知道</strong> InventoryAppService 应用服务及对应的消息契约对象之外,领域层包括应用层的其余代码若需要检查库存,都应调用属于防腐层的 InventoryClient 接口,从而隔离对上游库存上下文的依赖。当订单上下文与库存上下文之间的关系由进程内通信迁移为跨进程通信,即采用微服务架构风格时,只需要修改位于下游订单上下文基础设施层的 InventoryAppServiceClient 实现,保证了领域层的稳定性,将这一迁移带来的影响降到了最低。倘若还能在领域层严格遵循领域驱动战术设计的要求,做到领域模型与数据模型的隔离,降低风格迁移导致的数据库修改的成本,那么从单体架构向微服务架构的迁移就会变得相对容易。</p>
|
||||
<p>Martin Fowler 在 <em>MonolithFirst</em> 文章中谈到了将软件系统直接设计为微服务架构的担忧,如下图所示:</p>
|
||||
<p><img src="assets/d3101630-1a89-11ea-9b07-417001cef23a" alt="0.9365133227356852.png" /></p>
|
||||
<p><img src="assets/d3101630-1a89-11ea-9b07-417001cef23a" alt="png" /></p>
|
||||
<p>他给出的主要理由就是,设计者无法从一开始就确定稳定的微服务边界。一旦系统被设计为微服务,当微服务边界存在不合理之处时,对它的重构难度要远远大于单体架构。因此,他的建议是单体架构优先,通过该架构风格逐步探索系统的复杂度,确定限界上下文构成组件的边界(Component Boundaries),待系统复杂度增加证明了微服务的必要性时,再考虑将这些限界上下文设计为独立的微服务。倘若采用这样的架构演化路径,则如上所述的领域驱动设计实践就可以减低从单体架构迁移到微服务架构的成本,围绕着限界上下文设计架构,就要比直接围绕微服务进行设计要更加地稳健,是满足敏捷设计原则 <a href="https://www.martinfowler.com/bliki/Yagni.html">YAGNI(You Aren't Gonna Need It)</a>的正确选择。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user