干净的架构(The Clean Architecture),这是著名软件大师Bob大叔提出的一种架构,也是当前各种语言开发架构。干净架构提出了一种单向依赖关系,从而从逻辑上形成一种向上的抽象系统。
同⼼圆分别代表了软件系统中的不同层次,通常越靠近中⼼,其所在的软件层次就越⾼。基本上,外层圆代表的是机制,内层圆代表的是策略。 当然这其中有⼀条贯穿整个架构设计的规则,即它的依赖关系规则:源码中的依赖关系必须只指向同⼼圆的内层,即由低层机制指向⾼层策略。
- Entities(业务实体): 通过领域分析与建模,我们可以将领域实体以及针对这些领域实体的生命周期管理的相关业务逻辑,封装在这一层。关于领域实体对象的分析与识别过程,可参考 我另一篇文章《设计实战 – 场景分析 & 领域划分》
- Use Cases (用例): 用例层里一般放置和“特定场景”相关的逻辑,这些逻辑会对实体层定义的实体进行信息聚合。比如,我们以电商系统 “买家下单”这个场景为例,这个场景中,下单时会涉及到的实体有 商品对象、包裹、优惠券、买家信息、卖家信息、服务信息(比如配送服务)等。下单处理逻辑,需要对这些信息进行加工后,聚合成订单信息让用户确认或者创建。对这些实体根据特定场景进行编排的逻辑,它不属于任何一个实体所在的域。
- 接口适配器:将用例层的信息以某种形式对外暴露。比如,买家下单场景,我们可以以Restful的形式暴露接口,用于和Web前端集成等。
分层与逻辑构建划分的建议
在构建一个复杂的业务系统中,我们需要考虑对实体访问的API、SPI设计,也要考虑在场景中去沉淀可复用的业务资产。可复用的业务资产要能做到跨项目、跨客户复用,就必然需要预留可扩展点SPI。关于场景级开放,可参考 Lattice – 场景级SDK开放。 在经过笔者电信领域、电商领域、智慧城市领域多个大型项目的实践,同时参考了干净架构的一些建议,我总结了一套分层合理、非常适用于团队协作的逻辑工程划分方法。大体的工程结构和逻辑模块划分如下:
- 业务实体层:通过 domain-sdk 对外暴露实体服务的接口定义,以及在对实体处理过程中,会有一些特殊的定制要求,也以SPI方式暴露出去。SPI的表现形式,就是以扩展点的方式暴露出去,在运行期通过动态加载,获取具体的实现,从而实现对业务实体处理逻辑的扩展。
- 用例层:在用例层中,主要是由各类场景的定义以及可复用的业务资产组成。场景在执行的过程中,可以安装这些业务资产来满足业务的需要。同时,这些业务资产在跨项目、跨行业的复用过程中,也可以预留一些可扩展点,以SPI形式暴露出去。从而做到,项目级定制和平台逻辑的分离。
- 接口适配器(Endpoints):主要由各类web、interface等各类对外接口形式存在,用于适配更外层相关设备/系统的接入需要。
- 业务插件(Business Plugins):业务插件主要是实现业务资产的SPI,实现不同项目、不同行业中个性化业务逻辑的定制与增强。
以电商交易系统为例的工程划分样例
工程样例可以通过访问 https://github.com/hiforce/lattice-clean-arch-practice-sample 获取
层级 | 模块 | 工程 | 说明 |
---|---|---|---|
业务实体 | domain-item | domain-item-sdk | 商品域对外提供的SDK |
domain-item-common | 商品域对外提供通用模型 | ||
domain-item-service | 商品域的具体业务逻辑实现 | ||
domain-trade | domain-trade-sdk | 交易域对外提供的SDK | |
domain-trade-common | 交易域对外提供通用模型 | ||
domain-trade-service | 交易域的具体业务逻辑实现 | ||
业务用例 | place-order-scenario | 买家下单场景 | |
place-order-client | 买家下单场景对外提供的客户端(包含API) | ||
place-order-server | 买家下单场景的服务端实现 | ||
usecase-assets | 可复用的业务资产 | ||
presale-usecase-capability | presale-usecase-sdk | 预售业务能力对外提供的SDK | |
presale-usecase-capability | presale-usecase-server | 预售业务能力的服务端实现 | |
接口适配器 | sample-endpoints | 各类对外接入定义 | |
buynow-web | 模拟立即下单Web页面工程定义 | ||
业务定制层 | sample-business-apps | 各类业务定制化插件 | |
sample-business-a | 模拟A业务的业务定制 | ||
在这个工程中,我们运行org.hiforce.sample.buynow.web.starter.BuyNowWebStarter,然后打开浏览器输入 http://localhost:8080/buy/1 ,可以看到下面输出:
{"success":true,"orders":[{"orderId":1,"buyerId":"rocky","orderLines":[{"orderLineId":1,"itemId":"2919311334001001","buyQuantity":1,"unitPrice":4000}]}]}
继续输入http://localhost:8080/buy/2 , 可进一步观察输出的变化,如下:
{"success":true,"orders":[{"orderId":2,"buyerId":"rocky","orderLines":[{"orderLineId":2,"itemId":"2919311334001001","buyQuantity":1,"unitPrice":10000}]}]}
两个结果主要在商品单价上不一样,原因是业务定制包实现了预售的付款比例扩展点。 在不同场景下,预售资产会生效,从而导致单价会有不同。有兴趣的同学,可以自行debug做进一步观察。
扩展点在工程结构上的组织建议
业务实体层扩展点的定义建议
在实体层,一般我们会做一个领域扩展点门面,在这个扩展点门面下,会按照实体来进行分层定义。比如,在交易域,有Order、OrderLine等实体,那么交易域对外可扩展点门面,我们如下定义:
public interface TradeEntityExt extends IBusinessExt {
TradeOrderExt getTradeOrderExt();
TradeOrderLineExt getTradeOrderLineExt();
TradePageExt getTradePageExt();
}
这样定义的好处是,开发人员、第三方ISV通过浏览该领域的可扩展门面,就能很容易按照实体来找到其对应的扩展点。我们继续以OrderLine这个实体为例,这个实体对外提供的扩展点,我们再进一步按照实体的“行为”进行分类,如下:
public interface TradeOrderLineExt extends IBusinessExt {
/**
* @return The extension of Order Line entity in init behavior.
*/
TradeOrderLineInitExt getTradeOrderLineInitExt();
/**
* @return The extension of Order Line entity in check behavior.
*/
TradeOrderLineCheckExt getTradeOrderLineCheckExt();
/**
* @return The extension of Order Line entity in saving behavior.
*/
TradeOrderLineSaveExt getTradeOrderLineSaveExt();
}
当我们要想要对某个实体进行扩展时,很自然的一个想法就是要考虑是在对该实体做何种操作时需要扩展。比如,是实体在保存前要做个自定义加工处理,还是保存后要做个额外信息的补充?等等。在具体的实体行为扩展点门面下,才是具体的扩展点定义,比如上面OrderLine实体,我们在其“保存”行为上,定义如下扩展点:
public interface TradeOrderLineSaveExt extends IBusinessExt {
@Extension(reduceType = ReduceType.NONE)
Map<String, String> getCustomOrderLineAttributes(SaveOrderLineExtInput input);
@Extension(reduceType = ReduceType.NONE)
Void processOrderLineBeforeSaving(SaveOrderLineExtInput input);
@Extension(reduceType = ReduceType.NONE)
Void finalProcessOrderLineAfterSaving(SaveOrderLineExtInput input);
}
业务用例层扩展点的定义建议
和业务实体层不同,业务用例层里定义的场景,往往是跨业务活动的,是以业务流程形式进行组织与管理的。所以,在业务用例层,我建议扩展点门面是按照场景中所定义的业务活动来进行聚合。 我们在讨论需求时,往往会按照业务活动来讨论该如何实现需求。 比如,我们需要在 “买家下单”时,能够自定义某个下单提示组件;我们需要在 “卖家发货”时,能够定义最晚发货时长,等等。所以,按照“业务活动”来组织用例层的扩展点,会更加利于后期维护以及知识传播。
我们以预售交易业务资产为例,它对外提供的扩展点门面是 PreSaleTradeExt,定义如下:
public interface PreSaleTradeExt extends IBusinessExt {
/**
* @return The extensions of PreSale trade in buyer place order.
*/
PreSalePlaceOrderExt getPreSalePlaceOrderExt();
/**
* @return The extensions of PreSale trade in fulfillment.
*/
PreSaleFulfillmentExt getPreSaleFulfillmentExt();
/**
* @return The extensions of PreSale trade in refund.
*/
PreSaleRefundExt getPreSaleRefundExt();
}
我分别按照卖家下单、卖家履约、买家退款等几个业务活动进行了聚合。 我们进一步以买家下单为例展开,可以看到预售交易业务资产在这个业务活动上定义了以下扩展点:
public interface PreSalePlaceOrderExt extends IBusinessExt {
@Extension(name = "Custom PreSale Down Payment Ratio", reduceType = FIRST)
Double getCustomDownPaymentRatio(PreSaleOrderLine orderLine);
}
在预售这个场景,业务开发人员以及第三方ISV,可以很容易从预售交易SDK门面,按照业务活动快速找到这个扩展点,并实现业务定制。我以某业务A为例,他的定制实现如下:
@Realization(codes = SampleBusinessA.CODE)
public class BusinessAPreSaleExt extends BlankPreSaleTradeExt {
@Override
public BlankPreSalePlaceOrderExt getPreSalePlaceOrderExt() {
return new BlankPreSalePlaceOrderExt() {
@Override
public Double getCustomDownPaymentRatio(PreSaleOrderLine orderLine) {
return 0.4;
}
};
}
}
总结
干净的架构是笔者经历过各种大型工程项目,在实战中的确颇有感悟和心得的一种架构。基于Lattice框架,可以很好的进行业务扩展点的管理,以及业务资产沉淀。希望本文,能对各位程序员、架构师朋友们有所帮助。
2022-11-29 at 下午2:29
在默默关注,佩服笔者持之以恒精神。
2023-01-04 at 下午6:16
牛逼 看完后降维打击了属于是
2023-03-06 at 上午2:01
这几天看了下源码,了解了下里面调用链的执行过程,在invoke方法里面根据请求参数以及usercase的isEffect是否生效任何再决定是否走usecase的扩展点吗?
2023-03-06 at 下午2:48
是的,你可以尝试DEBUG一下。 因为业务在不同的上下文中,只有部分场景才会生效。 我举个例子:我们下订单,某个商品虽然即支持普通物流、也支持门店自提这两种场景,但在实际下单时,用户在下拉框选择”物流方式“时,就已经确定了哪种场景才会生效。
2023-06-06 at 下午7:01
受益匪浅,大佬法力无边,佩服,点赞
2023-09-18 at 下午2:29
大佬法力无边,佩服!
2023-09-18 at 下午3:53
可完美落地,提供了很好的指导,感谢。