系统设计的核心作用是在业务现实世界和抽象的IT实现之间建立起一道桥梁。
与其他行业被物理特性限制所束缚不同,软件世界可以变得无限庞大,而限制软件发展的其实是人的认知能力。所有软件设计服务的目标其实都是管理人的认知,是关于人有限的精力如何学习软件中无限多的知识(Knowledge)的问题。
软件行业从传统的瀑布开发模式,过渡到了敏捷开发模式,对于文档,敏捷宣言中说的是“工作的软件高于详尽的文档”,但实际工作中开发人员写的文档是越来越少,或者是能不写就不写;流程上,更是恨不得需求还没有出来就直接撸代码,撸完代码就直接上线。
缺乏整体系统设计,设计出来的系统就不够完善,再加上快速的系统迭代,导致系统越来越难以维护,开发成本越来越高,一个项目需要参与的人越来越多,最终没有人能够说明清楚,这个系统具体是如何运行的了。随着团队人员的更替,加上每个人的设计思路又不一样,更加加重了系统的复杂性。
上面也就引入了两个问题:
上面两个问题在MTDD都有相应的解法,后面我们会详细讲述,接下来我还是再详谈一下软件复杂度。
《软件设计哲学》这本书中提到,软件复杂度的三种症状:
为了从源头上解决这些问题,John Ousterhout教授提出:从项目一开始就要严格遵循进行软件设计的原则,那些为了赶工期而没有经过良好设计的代码,最终经过多次迭代后,都会变得越来越臃肿,继而变得再也无法维护了。
我非常认可John Ousterhout的观点,但
实际操作中发现基本不具有可行性,原因:
我的观点
对于“简化模块依赖”,“减少模糊性”,“高内聚低耦合”这些原则的话术,知道的人就知道怎么做,不知道的人还是不知道怎么做。这些术语缺少实际的指导性。
图片
一个有意思的现象:
图片
那系统到底是谁做出来的呢?(这里主要说的是业务系统。一些中间件之类的系统,基本都都由研发来完成的。)
图片
系统设计离不开,业务人员、产品经理以及技术研发的合作,业务和产品的需求没有理清楚,同样会导致系统复杂度提升。
图片
系统各主要相关方缺乏对系统设计的信息拉齐,给系统复杂度的提升同样有重要的贡献。
那么如何让各角色更好的进行信息对齐,这就引入了MTDD。
前面提到了《软件设计哲学》作者提出了一些系统设计总结,也有些人提出了一些方法论,比如领域驱动设计(DDD),测试驱动开发(TDD),行为驱动开发(BDD);但是这些模式,都是从设计方法论上给与指导,战术上指导偏少。下面我们来介绍我自己沉淀的一个方法论,和战术指导MTDD&MTDP。
MTDD的全称是:Module Tree Drive Design, 模块树驱动设计,也可以叫做能力树驱动设计。MTDD是一种系统设计模式,并同时提供了战术层的SDK。
MTDD主要思想是让业务,产品、研发共同对复杂业务系统中的模块进行分析,并对这些业务模块做好分层分类,最终形成各方达成一致的一棵模块树;研发人员开发可以通过给定的SDK,将系统中的代码按照模块的方式进行打标分类,系统根据代码中打标分类,自动生成一颗可视化的模块树。通过这个方式,让系统与业务保持高度一致。
MTDD从某个方面说也是DDD领域驱动设计中所宣导的统一语言一种实现方式。 |
MTDD思想主要体现在:
在系统设计中,模块是指一组相互依赖的程序元素,通常是在模块内部完成特定的工作。模块也可以被组合以形成更大的、更复杂的系统。子模块则是模块的一部分,通常是实现特定的功能。
在MTDD中, 模块化更多的是从业务的角度上来说的,比如一个营销触达模块,比如仓储系统中的入库模块;模块下面还可以有子模块,子模块也可以有子子模块, 这个可以根据一定的颗粒度进行灵活拆分,重点是业务,产品、研发三方达成一致,并且明确模块的关系(父子关系)。
将模块化的模板进行分层分类。
在系统设计中,分层和分类是非常重要的,这有助于提高系统的可扩展性和可维护性,也能很好的降低人的认知负荷。
分层设计:
在现实世界中,所有我们见到的事物,人类都自然的对其给与了分层,比如:
图片
再比如在仓储管理中,也会有天然的分层:
图片
分层分类-复杂度分解:
我们的目标是将一些同类型的提高一个抽象层次,将大脑比较难处理的9个概念简化成3个,这样就无需记忆其中的每个概念,另外由于较高层次的思想总是能够提示下面一个层次的思想,所以记忆起来会更方便。所有的思维过程(思考、记忆、解决问题)都应该使用这样的分组和概括的方法,将大脑中的无序信息组成一个相关关联的金字塔结构。
每个模块下又可以有多个子模块。
总而言之,设计人员对模块进行分层分类后,可以大大降低思考复杂度, 这个很好理解。
图片
分层分类的模块以树状接口进行可视化呈现。
左边图描述的是:业务、产品、研发对齐需求后,认为的业务系统上需要建设两个子模块的能力。右边的图是:研发实现产品需求后,系统自动生成的能力树的样子。
所有的业务配置,都是业务相关的,系统用来控制业务的逻辑,本质就是业务的一部分。在传统的业务系统中一般有两种方式来实现:
这两种方式都存在一些问题:
使用MTDD配置化能力时,就可以解决上面两个问题
业务配置一定属于具体的业务模块,因为配置是用户控制某个具体的模块逻辑,所以配置尽量挂在模块下面是一个非常自然的做法。
我的观点
业务配置一定属于具体的业务模块,因为配置是用户控制某个具体的模块逻辑,所以配置尽量挂在模块下面是一个非常自然的做法。
系统设计的核心作用是在业务现实世界和抽象的IT实现之间建立起一道桥梁。而业务系统本身就是现实世界在计算机系统中的映射。
现实世界是一个模块化的,层次化的树状结构,所以业务系统就应该自然的通过模块化的树状结构来进行映射。
MTDD正是基于此,通过一个可视化的能力树,这颗能力树作为实实在在,可以看得见的桥梁,来拉齐业务、产品和系统研发。并最终做到让业务和产品,可以真正以产品能力搭建的视角来规划,设计系统模块和系统功能。可以让系统架构人员自然而然的进行高内聚,低耦合的系统设计,可以让一线研发自然而然的进行模块化编程。
模块树驱动设计闭环
图片
DDD中也有统一语言,或者叫做“通用语言(Ubiquitous Language )”
当团队成员不能享用一个公共语言来讨论领域时,项目会面临严重的问题。领域专家使用自己的行话,技术团队成员在设计中也用自己的语言讨论领域。
代码可能是一个软件项目中最重要的产物,但每天用来讨论的术语却与代码中使用的术语脱节了。即使是同一个人都需要使用不同的 语言来交谈和书写,所以要想完成对领域的深刻表达通常需要产生 一种临时形式,但这种形式不会出现在代码甚至是书写的内容中。
在交流的过程中,需要做翻译才能让其他的人理解这些概念。开发 人员可能会努力使用外行人的语言来解析一些设计模式,但这并一定都能成功奏效。领域专家也可能会创建一种新的行话以努力表达 他们的这些想法。在这个痛苦的交流过程中,这种类型的翻译并不能对知识的构建过程产生帮助。
上面这段是话是摘自《领域驱动设计精简版 》
Eric Evans 早就意识到,需要在领域专家和研发之间共用一套通用语言,并且Eric Evans也做了大量的举例说明,来说明什么是通用语言,以及统一通用语言可以更好的服务于系统设计。
MTDD更也是站在巨人的肩膀上,提供了一个方法论:让业务,产品,技术在系统设计之前,一起对照系统模块树来进行沟通;对于一个新功能,一起思考是在某个模块下新增模块,还是修改货扩展模块内部的逻辑;在对齐后,就可以进行开发了,并且研发有一定的范式开做开发,开发后,系统的模块树就能够自动可视化的呈现出来;业务和产品也可以通过可视化的方式进行验收;
上面说了在业务方、产品、技术在参照能力树根据需求并对齐需要开发的模块后,研发可以按照一定的范式做系统开发;这是因为我们提供了一套开发的SDK,以及SDK的使用文档,来帮助研发人员来进行基于能力树功能的开发。系统功能开发完成后,相应的模块信息就可以自动在模块树页面上进行呈现。当然想要在页面上进行呈现,需要有前端来支持。
这个规范主要由几个主要的JAVA注解来实现:
4.1.3 系统的模块化以及分层分类
使用上面的java注解,对代码中模块进行打标。
业务模块化,并且做了分层与分类,那么系统中的代码需要根据业务中的分层分类进行进行分类打标,使其与业务分层分类保持一致。
我们这个世界够复杂了吧,如果让你设计一个IT系统来实现刻画这个世界的方方面面,我打赌一定没有人搞得定;但现实中的这个世界还是能够有条不紊的发展演进,没有需要出现“推倒重来”的现象,为什么呢, 我认为是我们的世界一直在用各种方式不停的重构。
“物竞天择,适者生存”出自达尔文的进化论,达尔文在1859年出版的《物种起源》一书中系统地阐述了他的进化学说。物竞天择,适者生存是指物种之间及生物内部之间相互竞争,物种与自然之间的抗争,能适应自然者被选择存留下来的一种丛林法则。
对于软件系统也是这样,业务是在不停的发展, 我们的认知也是一直不断的更新,当“我们”通过可视化的能力树发现一些突兀时,那肯定是某个或者某些模块拆分不正确,或者模块提供的能力不合适,这时,我们就可以考虑对模块树进行重构了,要么是拆分模块,要么是调整模块的关系,要么是修改模块的职责。
如果产品了解MTDD,那么就会提出更加符合产品化的需求了
如果研发对MTDD理解深入,那么当产品的需求不符合产品化,能力化时,就会与产品进行沟通,产品修改需求,以便更好的设计出产品化,系统能力化的需求。
✔好的产品需求 |
×不好的产品需求 |
|
针对特定的业务需求 定制系统能力 |
MTDP的全称是Module Tree Drive Programing, 领域树驱动编程。
注解@Module的定义
/**
*
* 模块注解,打在一个服务类上,Module注解是继承了Component注解,因此它注解的类可以被实例化到Spring中去
* 服务启动时会扫码所有Module类,将他们组装成树进行持久化。
*/
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
@Documented
public @interface Module {
/**
* 该模块的key
* 在设置模块的key的时候,在同一个服务里面(同一个根节点),如果两个类设置的key是一样,最后会被去重为一个模块,无论是单体服务还是分布式工程。
* moduleKey全局唯一,就算它们归属不同的根节点,也不能设置一样的moduleKey,
* 不同根节点的模块,如果设置了同一个moduleKey,后部署的服务将无法能力树的变更情况进行持久化。
* @return
*/
String moduleKey();
/**
* 用于指定该模块的父模块,
* 根节点的parentModule指定为 {@link Void}.class。
* 每个模块指定他们的夫模块,直到可达根节点,最后生成一颗树。
* 指定parentModule时一定要注意,不能循环依赖了,循环依赖的情况下,服务将抛出异常无法启动,
* @return
*/
Class<?> parentModule();
/**
* 该模块名称
* @return
*/
String moduleName();
/**
* 模块描述,一个该模块详细的描述
* @return
*/
String moduleRemark() default "";
/**
* 能否被剪枝,你是可以设置模块能不能被剪枝,默认是不可以的(后续会根据数量对比情况进行调整默认值)。
* 我们应该将系统中必要的功能设置为无法裁减,将那些加强性的能力,智能化的能力,衍生化的能力,非基本的能力设置为可以裁减
* @return
*/
boolean cutAble() default false;
}
模块的具体例子:
@Module(moduleKey = "scm.wms", moduleName = "WMS", parentModule = NULL)
public class WmsModule {
//业务逻辑
}
@Module(moduleKey = "scm.wms.inner.test1", moduleName = "测试模块1", parentModule = WmsModule.class)
public class InnerTestModule1 {
//业务逻辑
}
@Module(moduleKey = "scm.wms.inner.test2", moduleName = "测试模块2", parentModule = WmsModule.class)
public class InnerTestModule2 {
//业务逻辑
}
每个模块都有父模块ParentModule,跟模块的父模块为NULL,模块树则是由所有模块组成的一个树状结构的树。
图片
图片
找到模块上的配置,点击展开配置
图片
展开配置如下:
图片
上面的配置,是无需前端进行开发,只需要后端实现就好,后端代码:
@Data
@ModuleConfig(configKey = "scm.wms.inbound.receive.oneBarcodeMuliSku", configName = "收货一码多品配置", module="scm.wms.inbound.receive")
public class ReceiveScanOneCodeMoreSkuModuleConfig {
@ConfigItem(itemName = "是否开启一码多品的拦截"
, itemRemark = "如果关闭, 一码多品的商品在收货时, 不会收到"该条码对应多个商品,请打印商品标签"的提醒, prd:https://poizon.feishu.cn/wiki/wikcnstRj3Qfbn4fXDrmvBk6R0d"
, defaultValue = "true")
private Boolean isOpen;
@ConfigItem(itemName = "拦截方式"
, itemRemark = "如果配置强拦截,则必须打印商品标签, 阻断收货流程。如果配置弱拦截, 只是提醒一下, 不阻断收货流程"
, scopeClass = InterceptType.class
, defaultValue = "WEAK")
private String interceptType;
@ConfigItem(itemName = "拦截规则"
, scopeClass = InterceptRuleType.class
, defaultValue = "ARBITRARY")
private String interceptRule;
}
配置的几个java注解:
/**
* 业务配置注解,业务配置是Module的字段
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModuleConfig {
/**
* 配置的key
*/
String configKey();
/**
* 配置名称
* @return
*/
String configName();
/**
* 配置描述,鼠标悬浮时的气泡提示
* @return
*/
String configRemark() default "";
/**
* 是否必要,必要的不可以被剪枝
* @return
*/
boolean cutAble() default false;
}
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigItem {
/**
* 配置名称
* @return
*/
String itemName();
/**
* 配置描述,鼠标悬浮时的气泡提示
* @return
*/
String itemRemark() default "";
/**
* 默认值
* @return
*/
String defaultValue() default "";
/**
* 如果你的取值范围是枚举指定枚举的全路径类名,
* 如果你的取值范围是一种策略,指定策略的interfaces的全路径名称
* @return
*/
Class<?> scopeClass() default Void.class;
/**
* 范围,默认通用,当你需要定制时才需要指定
* @return
*/
ScopeType scopeType() default ScopeType.COMMON;
/**
* 自定义组件,需要前端参与开发,非特殊组件,无需设置,DSK会自动给你寻找组件
* @return
*/
String customScopeType() default "";
/**
* 对位文本类型的配置,min则为文本长度限制的最小值
* 对于数字类型的配置,min则为数字的最小值。
*/
int min() default Integer.MIN_VALUE;
/**
* 对位文本类型的配置,min则为文本长度限制的最大值
* 对于数字类型的配置,min则为数字的最大值。
*/
int max() default Integer.MAX_VALUE;
}
TMF 是 Trade Modularization Framework 的全称,即交易模块化框架,最初是交易系统中的一个代码模块,后来剔除业务耦合部分,独立出来成为一个实现业务与平台分离的业务框架。
图片
(图片来源:https://www.cnblogs.com/shoshana-kong/p/14957739.html)
图片
(图片来源:https://www.infoq.cn/article/w3ztwqs9q4astbksd0mj)
图片
前面有提到“持续重构”这个概念;但是持续重构提出来很容易,但是做起来,就没有这么简单;
What:首先是如何发现需要重构的点,为什么是这个点要重构,而不是那个点要重构。
When:其实是什么时候需要进行重构。
为了更好的回答上面两个问题,个人认为最重要的是能够量化两个模块的复杂度。
✔:高内聚低耦合,但是耦合度到底高还是低,如何衡量。想要衡量,就需要做到数据化,指标化。
图片