软件工程的核心:业务流程化、节点领域化
核心观点:
《软件方法》一书中说:关于改进业务序列图,找到系统竞争力一般有三种改进措施。
改进模式一:物流变成信息流。
- 举例:报纸变成门户网站
改进模式二:改善信息流转。
- 举例:有一个聚合网站把淘宝和京东的信息聚合起来展示
改进模式三:封装领域逻辑。
- 举例:销售员自行选择供应商 转变成 系统录入各个供应商的各种信息,由系统判断,应该选择哪一个供应商。
软件工程核心目标是实现业务需求的核心愿景。
而核心愿景的实现借助业务流程在软件中的映射实现。
在工程设计(代码实现方面)的核心有两点:
- 业务流程化
- 节点领域化
软件工程的核心:业务流程化,节点领域化。 这个是我今天跟一个朋友讨论工程的时候。我突然里面迸发出来的一句话。
比如:我以前曾经做了支付宝内容中台审核平台的代码重构工作。现在回顾起来。业务流程化,和节点领域化分别在我的代码和产品中都有所体现。
- 可视化的流程编排,反映“业务流程化”
- 节点的多态处理,体现“节点领域化”
业务流程化
反例
先举一个反例。
public void process() {
// 校验
validate();
// 转化
convert();
// 保存到数据库
save();
}
public void process() {
// 校验
validate();
// 转化
convert();
// 保存到数据库
save();
}
2
3
4
5
6
7
8
这个就是没有一点流程化。貌似已经流程化。但是,这个处理是任何代码都可以写成这种格式。
这不叫业务流程化。所有流程都这么做,那就完全反映不出来业务的流程。
正例
一文教会你如何写复杂代码 中的代码示例
为例
@Phase
public class OnSaleProcessPhase {
@Resource
private PublishOfferStep publishOfferStep;
@Resource
private BackOfferBindStep backOfferBindStep;
//省略其它step
public void process(OnSaleContext onSaleContext){
SupplierItem supplierItem = onSaleContext.getSupplierItem();
// 生成OfferGroupNo
generateOfferGroupNo(supplierItem);
// 发布商品
publishOffer(supplierItem);
// 前后端库存绑定 backoffer域
bindBackOfferStock(supplierItem);
// 同步库存路由 backoffer域
syncStockRoute(supplierItem);
// 设置虚拟商品拓展字段
setVirtualProductExtension(supplierItem);
// 发货保障打标 offer域
markSendProtection(supplierItem);
// 记录变更内容ChangeDetail
recordChangeDetail(supplierItem);
// 同步供货价到BackOffer
syncSupplyPriceToBackOffer(supplierItem);
// 如果是组合商品打标,写扩展信息
setCombineProductExtension(supplierItem);
// 去售罄标
removeSellOutTag(offerId);
// 发送领域事件
fireDomainEvent(supplierItem);
// 关闭关联的待办事项
closeIssues(supplierItem);
}
}
@Phase
public class OnSaleProcessPhase {
@Resource
private PublishOfferStep publishOfferStep;
@Resource
private BackOfferBindStep backOfferBindStep;
//省略其它step
public void process(OnSaleContext onSaleContext){
SupplierItem supplierItem = onSaleContext.getSupplierItem();
// 生成OfferGroupNo
generateOfferGroupNo(supplierItem);
// 发布商品
publishOffer(supplierItem);
// 前后端库存绑定 backoffer域
bindBackOfferStock(supplierItem);
// 同步库存路由 backoffer域
syncStockRoute(supplierItem);
// 设置虚拟商品拓展字段
setVirtualProductExtension(supplierItem);
// 发货保障打标 offer域
markSendProtection(supplierItem);
// 记录变更内容ChangeDetail
recordChangeDetail(supplierItem);
// 同步供货价到BackOffer
syncSupplyPriceToBackOffer(supplierItem);
// 如果是组合商品打标,写扩展信息
setCombineProductExtension(supplierItem);
// 去售罄标
removeSellOutTag(offerId);
// 发送领域事件
fireDomainEvent(supplierItem);
// 关闭关联的待办事项
closeIssues(supplierItem);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
这就是把业务给流程化。
这个代码,绝大多数初学者也能看懂。初学者也能看懂的代码未必不是好代码。 而且这个代码,即使产品来看,也大概能够理解其中的业务处理的流程每一步都做了哪些事情。
这就是 骨架 + 皮肉 的皮肉,骨架就是,有多少个节点。每个节点到底是在做什么事情。
除非你的应用有极强的流程可视化和编排的诉求,否则我非常不推荐使用流程引擎等工具。
-- COLA作者《疑问教会你如何写复杂业务的代码》
流程编排下的做法
笔者曾经在蚂蚁金服,做过内容中台审核平台的可视化编排以及可视化审核通过条件设置方式。
首先,背景,是需要接入更多的场景化审核条件支持,以及更多的部门接入内容中台。那么,原有的代码写死的审核流程不符合未来的发展。所以,我们启用了新一轮的重构。而这一次的技术选型已经整体设计是我来做的。
我的分析是这样子的:
第一步:先有一个抽象的对象。 内容(属性:图文、视频、纯文本)
第二步:标识出来所有的,可配置的审核通过条件
第三步:抽象出来,内容应该流入审核流程的路由标识
第四步:根据标识选择审核预处理的所有原子组件
第五步:基于路由,选择预处理的原则组件集合。顺序处理(同步、异步)
第六步:审核预处理组件处理完毕之后,根据运营人员配置的对应路由标识下的通过规则,判断对应内容是否审核通过。
核心类图是:
这是后端的架构的处理类图。但其实到这一步,其实还是远远不够的,还需要一整套的产品流程,才叫完整的业务流程化。
比如,我们可以让运营,配置好路由规则之后,在预发环境,验证路由规则是否生效。判断某个文章是否走了某个路由。如果验证正确一键从预发环境同步到生产环境。
然后,我们前端通过,所有内容指标项的勾选,然后,配置上,所有的数值比值操作 >
、=
、<
、>=
、 <=
等等,判断操作 是
、否
等。基于前端的运营的配置,自动生成我的 velocity 脚本,用 velocity 脚本表达式,去计算出需要的被判断属性的值。
然后通过,前端的可视化展示,给前端展示出来,他对应的某个路由下的所有规则。
脚本的生成、测试、上线全部全部由运营完成。
我想说:事件风暴式的领域驱动设计就是伪命题。 因为它会导致业务没有流程化了。
节点领域化
反例
public class Object {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
public class Object {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
2
3
4
5
6
7
8
9
10
节点领域化,就是,我们将具体的前边业务流程化的每个流程节点,进行领域内部逻辑的封装,也就是把这个对象的某些特性的封装在对象之内。比如 内容:是否是一个文本,内容是否是一个图文视频。
然后就实现了节点领域化。但是又有人又要说了,那我这个对象的特性如果是依赖外部条件来判断,那我是否能把这个领域特性封装在这个对象内部,如果可以那该怎么做呢?
对象的特性,是对象本身的方法。这个特性不会独立于这个对象来存在的情况下,那么这个对象,有一个方法返回这个特性的值,从面向对象的角度来分析,是没有违反设计原则的,当然,你可以进行反驳。
那么我们来考虑,我这个对象,依赖外部依赖来判断我当前的值,那我应该怎么办?
第一种方法:借助函数式编程
可以在外部,构建,对应特性计算的计算类,内部有一个方法,是计算该对象的某个特性,然后,在引用这个对象时,将一段函数计算,作为参数传到当前的对象的参数方法中,这个,可以首先,对象特性的方法的外置,也可以实现对象中有了加载该特性的方法。当然,我是觉得这种方式不够,面向对象,因为这个面向对象我这个属性值的计算逻辑不应该以参数的形式传进来。因为我这个属性值就不需要方法啊
public class Content {
public boolean passRiskControl(Function<Content, Boolean> passRiskFunction) {
return passRiskFunction.apply(this);
}
}
@Compontent
public class PassRiskPreCheck {
// 依赖的外部接口
@Resourse
private RemoteRiskControl remoteRiskControl;
public void process(Content content) {
Function<Content, Boolean> passRiskFunction = (innerContent) -> {
return remoteRiskControl.checkRisk(innerContent);
}
return content.passRiskControl(passRiskFunction);
}
}
public class Content {
public boolean passRiskControl(Function<Content, Boolean> passRiskFunction) {
return passRiskFunction.apply(this);
}
}
@Compontent
public class PassRiskPreCheck {
// 依赖的外部接口
@Resourse
private RemoteRiskControl remoteRiskControl;
public void process(Content content) {
Function<Content, Boolean> passRiskFunction = (innerContent) -> {
return remoteRiskControl.checkRisk(innerContent);
}
return content.passRiskControl(passRiskFunction);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
第二种方法:借助Spring静态上下文工具类
借助框架有两种方式,一种是,使用 上下文静态的方法获取到外部依赖的处理,这个方式我个人方式是不推荐的。因为这种方法会导致依赖关系不明确,就是,其实我是不知道我当前对象依赖了哪些外部的资源,静态上下文的方式,这个问题尤其严重。
public class Content {
public boolean passRiskControl() {
return BeanUtils.getBean(PassRiskPreCheck.class)
.checkRisk(innerContent);
}
}
// 依赖的外部接口
@Compontent
public class PassRiskPreCheck {
@Resource
private RemoteRiskControl remoteRiskControl;
public boolean checkRisk(Content content) {
return remoteRiskControl.checkRisk(innerContent);
}
}
public class Content {
public boolean passRiskControl() {
return BeanUtils.getBean(PassRiskPreCheck.class)
.checkRisk(innerContent);
}
}
// 依赖的外部接口
@Compontent
public class PassRiskPreCheck {
@Resource
private RemoteRiskControl remoteRiskControl;
public boolean checkRisk(Content content) {
return remoteRiskControl.checkRisk(innerContent);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
第三种方式:借助Spring的原型实例实现外部依赖的依赖注入
首先,我们的要知道,这个对象的生命周期,每次业务流程开始会创建,然后处理流程结束之后会销毁。这是对象的生命周期,然后我们可以同时进行多个对象的同时处理。这个对应到Spring的实例实例中就是原型的实例。
在Spring去加载对应的原型实例对象过程中,实现外部依赖的依赖注入。这是一个很巧妙的方式。我是觉得这种方式特别推崇。
既保留了面向对象中,该对象的基本属性,又保证了外部依赖关系的显性化。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public @interface DomainEntity {
}
public class DomainFactory {
public static <T> T get(Class<T> entityClz){
return ApplicationContextHelper.getBean(entityClz);
}
}
@DomainEntity
public class Content {
@Resourse
private RemoteRiskControl remoteRiskControl;
public boolean passRiskControl() {
return remoteRiskControl.checkRisk(this);
}
}
public class ProcessClass {
public void processor() {
// 原型 Bean
Content context = DomainFactory.get(Content.class);
// 内置方法
context.passRiskControl();
}
}
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public @interface DomainEntity {
}
public class DomainFactory {
public static <T> T get(Class<T> entityClz){
return ApplicationContextHelper.getBean(entityClz);
}
}
@DomainEntity
public class Content {
@Resourse
private RemoteRiskControl remoteRiskControl;
public boolean passRiskControl() {
return remoteRiskControl.checkRisk(this);
}
}
public class ProcessClass {
public void processor() {
// 原型 Bean
Content context = DomainFactory.get(Content.class);
// 内置方法
context.passRiskControl();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
总结
业务流程化、节点领域化
好了,本文介绍完了,什么叫业务流程化,什么叫节点领域化。
借助这 十字真言,相信你必定可以在应对复杂业务的过程中得心应手。