如何正确地理解应用架构并开发(上):https://developer.aliyun.com/article/1443483
应用架构的核心
纵观上面介绍的所有应用架构,我们可以发现一个共同点,就是“核心业务逻辑和技术细节分离”。
是的,六边形架构、洋葱圈架构的核心职责就是要做核心业务逻辑和技术细节的分离和解耦。
试想一下,业务逻辑和技术细节糅杂在一起的情况,所有的代码都写在ServiceImpl里面,前几行代码是做validation的事,接下来几行是做convert的事,然后是几行业务处理逻辑的代码,穿插着,我们需要通过RPC或者DAO获取更多的数据,拿到数据后,又是几行convert的代码,在接上一段业务逻辑代码,然后还要落库,发消息.....等等。
再简单的业务,按照上面这种写代码的方式,都会变得复杂,难维护。
因此,我认为应用架构的核心使命就是要分离业务逻辑和技术细节。让核心业务逻辑可以反映领域模型和领域应用,可以复用,可以很容易被看懂。让技术细节在辅助实现业务功能的同时,可以被替换。
如何正确的进行工程划分?
这些应用架构思想虽然很好,但我们很多同学还是“不讲Co德,明白了很多道理,可还是过不好这一生”。
所以我们接下来聊聊这个偏工程实践的话题。
▐ 业务工程中最基本的Module划分
上文提到了Moudle和包之间的关系,我们有个基本共识,Module比Package(包)要大一些。那我们基本的分层结构又应该是怎么样的呢,这里列两种业务工程中最常见的Module之间的依赖关系:
- 非依赖倒置下的Module分层
- 依赖倒置(DIP)下的Module分层(推荐)
依赖倒置(DIP):
简单说依赖倒置就是你不要直接依赖我,你和我都同时依赖一个接口(所以有时候也叫面向接口的编程),这样我们之间就解耦了,依赖和被依赖方都可以自由改动了。
- 依赖倒置下的Module分层的好处
- Domain层会变得更加纯粹,完全摆脱了对技术细节(以及技术细节带来的复杂度)的依赖,只需要安心处理业务逻辑就好(我们经常说的核心业务逻辑稳定其实就是Domain层的稳定,上文中的Domain层完全依赖于接口和内部的模型,屏蔽了技术细节,如果我们更换技术组件,也只需要更换技术细节的实现即可)
- 并行开发:只要在Domain和Infrastructure约定好接口,可以有两个同学并行编写Domain和Infrastructure的代码
- 可测试性:没有任何依赖的Domain里面都是POJO的类,单元测试将会变得非常方便,也非常适合TDD的开发。
分层职责
1.适配层(Adapter Layer):负责对前端展示(web,wireless,wap)的路由和适配,对于传统B/S系统而言,adapter就相当于MVC中的controller;
○用户展现信息以及解释用户命令
○大部分工程为start模块
2.应用层/应用服务层(Application Layer):主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的
○应用层也可以绕过领域层,直接访问基础实施层(CQRS模式中对于查询的说法就是可以绕过DomainModel之间查询数据);
○很薄的一层,用来协调应用的活动。它不包含业务逻辑
○同样的一个Service应可被不同的Adapter(Web、远程调用、异步消息)复用)。
○适合处理事务、高层次日志(oplog)、安全(权限)
3.领域层(Domain Layer):主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Domain Entity)的方法对App层提供业务实体和业务逻辑计算。
○领域是应用的核心,不依赖任何其他层次
○通常每一个聚合(aggregate)一个package。聚合包含实体(entity),值对象(value object),领域事件(domain event),资源库(repository,仅接口)接口和一些工厂(Factory)。
4.基础实施层(Infrastructure Layer):主要负责技术细节问题的处理,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。此外,领域防腐的重任也落在这里,外部依赖需要通过防腐层(Anti-Corruption) 实现的转义处理,才能被上面的App层和Domain层使用。
▐ 如果我要提供一个SDK对外提供服务怎么办?
Module的定义应该遵循自身业务需求(如无必要勿增实体)
上文中如果需要一个api包则增加一个API Module包即可
这里的client包不要依赖Domain,这会引入大量无关依赖,对包的使用者造成困扰
▐ 包结构的划分
分层是属于大粒度的职责划分,太粗,我们有必要往下再down一层,细化到包结构的粒度,才能更好的指导我们的工作。
还是拿一堆玩具举例子,分层类似于拿来了一个架子,分包类似于在每一层架子上又放置了多个收纳盒。所谓的内聚,就是把功能类似的玩具放在一个盒子里,这样可以让应用结构清晰,极大的降低系统的认知成本和维护成本。
那么,对于一个后端应用来说,应该需要哪些收纳盒。
各个包结构的简要功能描述,如下表所示:
- 你可能会有疑问,为什么Domain的model是可选的?
因为是应用架构,不是DDD架构。对于应用架构来说:无有必要勿增实体。领域模型对设计能力要求很高,没把握用好,一个错误的抽象还不如不抽象,宁可不要用,也不要滥用,不要为了DDD而DDD。
问题的关键是要看,新增的模型没有给你带来收益。比如有没有帮助系统解耦,有没有提升业务语义表达能力的提升,有没有提升系统的可维护性和可测性等等。
模型虽然可选,但DDD的思想是一定要去学习和贯彻的,特别是统一语言、边界上下文、防腐层的思想,值得深入学习,仔细体会。实际上,应用架构里面的很多设计思想都来自于DDD。其中就包括领域包的设计。
前面的包定义,都是功能维度的定义。为了兼顾领域维度的内聚性,我们有必要对包结构进行一下微调,即顶层包结构应该是按照领域划分,让领域内聚。
也就是说,我们要综合考虑功能和领域两个维度包结构定义。按照领域和功能两个维度分包策略,最后呈现出来的,是如下图所示的顶层包节点是领域名称,领域之下,再按功能划分包
下图是一个例子,按照分包策略,我们在每一个Module下面首先按照领域做一个顶层划分,然后在领域内,再按照功能进行分包。
如何解决应用架构中的问题
▐ 应用架构中如何处理解耦问题
“高内聚,低耦合”这句话,你工作的越久,就越会觉得其有道理。
所谓耦合就是联系的紧密程度,只要有依赖就会有耦合,不管是进程内的依赖,还是跨进程的RPC依赖,都会产生耦合。依赖不可消除,同样,耦合也不可避免。我们所能做的不是消除耦合,而是把耦合降低到可以接受的程度。在软件设计中,有大量的设计模式,设计原则都是为了解耦这一目的。
在DDD中有一个很棒的解耦设计思想——防腐层(Anti-Corruption),简单说,就是应用不要直接依赖外域的信息,要把外域的信息转换成自己领域上下文(Context)的实体再去使用,从而实现本域和外部依赖的解耦。
- 防腐层设计在应用架构中的应用
我们把防腐层(Anti-Corruption)这个概念进行泛化,将数据库、搜索引擎等数据存储都列为外部依赖的范畴。利用依赖倒置,统一使用防腐层(Anti-Corruption)/Repository等接口来实现业务领域和外部依赖的解耦。
其实现方式如下图所示,主要是在Domain层定义防腐层(Anti-Corruption)/Repository等接口,然后在Infrastructure提供防腐层(Anti-Corruption) 接口的实现。
- 高内聚低耦合在包划分中的体现
刚刚我们提到了在包划分的过程中,推荐先按照领域分包,这其实就是一种高内聚低耦合,这样做的好处有以下几方面:
- 明确这个包下面提供的类和功能应该放在一起考虑
- 通过包的可见性现在自身领域服务(功能)对外的可见性(重点)
a.写java的同学经常会给领域模型DomainModel上直接加上Setter方法,这样会比较方便构造领域对象,但是这样的做法其实违反了开闭原则,意味着你的领域对象中的所有属性对外都是暴露的,可以直接通过set的方式而不是通过领域对象提供的方法来进行变更,这对领域对象的稳定性来说很不友好b.通过包的可见性来暴露自身想暴露的能力这里推荐一种比较好的做法:
- 对于setter方法以及内部调用的方法设置为包可见(package private)
- 通过构造函数/Factory(复杂的聚合根)创建领域对象
▐ 如何对齐文中的概念和实际业务中的Module?
由于各个团队之间自身规范的问题以及建项目的同学自身对应用架构的理解不同,你可能会发现如下几种情况:
业务系统中的Module层数和文中描述的不同
- 业务复杂度决定了包的层级,对于简单的web系统,单module其实更能降低架构复杂度
- 拆分粒度不同,有些工程可能会拆分出类似Infrastruct-client(对应上文中的防腐层(Anti-Corruption)Interface)这样的Module
- 业务诉求不同:有些业务系统并不需要提供API,而是通过DTS/Controller驱动业务,自然也就不需要API Module
业务系统中的Module命名和文中描述不同
- 每个人对module的命名问题,实际还是要看这个module承载的业务职责
- 模块的职责不同,比方说对外提供的富客户端一般以Client命名,接口包一般就叫API了
业务系统中Module的依赖关系和文中不同
- 有没有可能是分层太多倒置后来的同学也搞不清楚该怎么依赖了,导致的错误依赖
- 文中也提供了好几种依赖,就像地心说和日心说一样,对于应用结构的认识是有一个过程的
业务系统中的包划分和文中不同
- 很多业务系统直接按照功能分包了,比如repository放一起(我认为对于实现来说是ok的,对于领域模型来说还是要有领域的概念)
- 那么如何找到这个工程目前的工程规范呢?
- 如果有工程结构的文档,优先阅读(省时间)
- 找一个Use Case,从入口侧去看文中的业务开发中涉及到的业务逻辑写在了哪个包中
注:Use Case是《架构整洁之道》里面的术语,简单理解就是响应一个Request的处理过程
▐ 如果业务代码中的应用架构不合理,你有自己的想法怎么办?
- 工程刚开始搭建阶段:大家团队内进行讨论,并产出应用架构图,统一领域语言:module分布、包命名规范、类命名规范等,再进行开发
- 工程不复杂,且大家都认可新的应用架构:拉分支修改,改造后进行功能的回归测试,组内review完没问题将变更合并入主干分支
- 工程比较复杂或者你只是想让你这部分代码符合你的规范:并不建议这样做,对于一个应用工程来说,不同代码风格往往比合理的工程架构更致命,会导致工程结构混乱,大家理解困难,一个统一的工程规范远比合理更重要
如何编写业务代码?
▐ DDD下业务代码的组织方式
- “写操作”在DDD中的实现3种场景,分别是
- 通过聚合根完成业务请求,这是DDD完成业务请求的典型方式
- 通过Factory完成聚合根的创建
- 通过DomainService完成业务请求
- 读操作,同样给出了3种方式
- 基于领域模型的读操作(读写操作糅合在了一起,不推荐)
- 基于数据模型的读操作(绕过聚合根和资源库,直接返回数据,推荐)
- CQRS(读写操作分别使用不同的数据库,比较重)
(图中的ReadModel可以理解为DTO)
- 对于业务代码来说,怎么提炼出业务逻辑和技术细节?
- 对Use Case进行过程分解
- 对象模型建设
举个例子,在上架过程中,有一个校验是检查库存的,其中对于组合品(CombineBackOffer)其库存的处理会和普通品不一样。原来的代码是这么写的:
boolean isCombineProduct = supplierItem.getSign().isCombProductQuote(); // supplier.usc warehouse needn't check if (WarehouseTypeEnum.isAliWarehouse(supplierItem.getWarehouseType())) { // quote warehosue check if (CollectionUtil.isEmpty(supplierItem.getWarehouseIdList()) && !isCombineProduct) { throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "亲,不能发布Offer,请联系仓配运营人员,建立品仓关系!"); } // inventory amount check Long sellableAmount = 0L; if (!isCombineProduct) { sellableAmount = normalBiz.acquireSellableAmount(supplierItem.getBackOfferId(), supplierItem.getWarehouseIdList()); } else { //组套商品 OfferModel backOffer = backOfferQueryService.getBackOffer(supplierItem.getBackOfferId()); if (backOffer != null) { sellableAmount = backOffer.getOffer().getTradeModel().getTradeCondition().getAmountOnSale(); } } if (sellableAmount < 1) { throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "亲,实仓库存必须大于0才能发布,请确认已补货.\r[id:" + supplierItem.getId() + "]"); } }
然而,如果我们在系统中引入领域模型之后,其代码会简化为如下:
if(backOffer.isCloudWarehouse()){ return; } if (backOffer.isNonInWarehouse()){ throw new BizException("亲,不能发布Offer,请联系仓配运营人员,建立品仓关系!"); } if (backOffer.getStockAmount() < 1){ throw new BizException("亲,实仓库存必须大于0才能发布,请确认已补货.\r[id:" + backOffer.getSupplierItem().getCspuCode() + "]"); }
有没有发现,使用模型的表达要清晰易懂很多,而且也不需要做关于组合品的判断了,因为我们在系统中引入了更加贴近现实的对象模型(CombineBackOffer继承BackOffer),通过对象的多态可以消除我们代码中的大部分的if-else
如何正确地理解应用架构并开发(下):https://developer.aliyun.com/article/1443481