如何减少软件设计和实现之间鸿沟
在软件领域,有一个古老的神话:即我能保证设计和代码实现完全一致。这的确是一个非常有价值的目标。试想下,如果我们的系统毫无设计,或者设计和代码实现毫无关联,在当今软件如此复杂的情况下,其实现和维护难度可想而知。
本文将结合我最近给ICT做软件设计培训的一些感悟,尝试介绍一些减少设计和实现之间鸿沟的方法,这些方法包括语言一致,设计一致和代码一致。除了介绍方法之外,我会以运营商计费系统为实践案例,辅助讲解如何落地一致性设计。诚然,完美的设计和代码一致的确是神话(和人月神话一样),因此,本文的立意并非是要指导你变成“神”,而是期望在减少设计和实现不一致的征途中,能迈出一小步。
一、语言一致
“语言、语言、语言”,重要的事情说三遍,我一直在不遗余力的强调语言的重要性,是因为语言是一切的基础,正如维特根斯坦所说,“语言边界决定了我们的思维边界”。
要保证设计和实现的一致,首先要保证的,就是概念的完整性和语言的一致性。如果语言都做不到一致,其他的都是扯淡。
1. 统一语言
具体而言,我们需要保证“沟通语言,文档语言,设计语言,代码语言”的一致性。我们可以通过统一语言来做到这一点,统一语言(Ubiquitous Language)这个概念来自于Eric Evans的著作《Domain Driven Design》,其本人在谈到DDD核心的时候,也总是把“统一语言”放在第一位。
形成统一语言的关键就在于我们要制定一个领域词汇表,把领域中,涉及到的核心概念,用英文、中文、解释的形式无歧义的规定下来。这个词汇表作为团队的“共识”,就是true of source,后续的活动都应该严格遵循之。
沟通语言:就是我们日常交流、会议中使用的语言,大家都应该使用领域词汇表中的语言来沟通,否则就会出现很多的鸡同鸭讲,你就需要大把的时间去澄清概念。
文档语言:不管是需求文档,设计文档,测试文档等等,里面的语言都应该和领域词汇表保持一致。
设计语言:不管是用例图、模型图、流程图、类图、时序图等等设计中,其语言也应该和领域词汇表保持一致。
代码语言:最后,也是最重要的。在工程落地的时候,我们的代码当然也要和领域词汇表保持一致。除此之外,我们的代码还要和设计意图保持一致。
2. 计费系统
在我进入的任何一个项目的第一件事,无一例外,都是从领域概念开始的。首先我需要理解领域,然后挖掘概念,然后和团队一起形成领域词汇表(统一语言)。很多接受过我辅导的团队,事后回顾的时候,也都表示“统一语言”是对他们帮助最大的点之一。
接下来,我们以运营商计费系统为例,看看统一语言到底是什么,这个需求是这样的:
运营商向用户提供电话服务,支持用户拨打/接听电话,并对通话收取费用。如:主动拨打电话收取 0.5 元/分钟的通话费用;接听电话收取 0.4 元/分钟的通话费用。
运营商为了吸引客户,定义了若干电话套餐,总共有三种类型的套餐。这三个套餐分别是:
基础套餐:主叫收费 0.5 元/分钟;被叫收费 0.4 元/分钟
固定时长套餐:套餐月固定费 100 元,包含:200 分钟主叫通话时间+200 分钟被叫接听时间
家庭套餐:套餐月固定费 20 元,用户可以指定 N 个号码作为自己的亲情号,用户接听/拨打亲情号均不收费
我们要设计一个 计费系统 用于套餐计费规则的执行,保存计费记录,并通知账户系统扣减费用。注意:在一次通话过程中,通话控制系统可能会调用多次计费系统进行计费。
这个需求大家应该不陌生,就是我们日常在用的,通过分析需求,我们不难把里面的一些核心领域概念,诸如计费(Charge)、账户(Account)、计费套餐(ChargePlan)、计费规则(ChargeRule)等梳理出来,形成如下的领域词汇表。
英文 |
中文 |
解释 |
Charge |
计费 |
运营商对用户的通话进行计费。 |
Charge Record |
计费记录 |
每一次计费,都会生成一条计费记录 |
Session |
通话 |
一次通话,可能会产生多条Charge Record |
Account |
账户 |
用户在运营商开通,账户里有用户套餐信息和金额信息。 |
Charge plan |
计费套餐 |
是套餐这个概念,在计费系统中的映射。 |
BasicChargePlan |
基础套餐 |
|
FixedTime ChargePlan |
固定时长套餐 |
|
FamilyChargePlan |
家庭套餐 |
|
Charge rule |
计费规则 |
不同的套餐对应的是不同的计费规则 |
Duration |
通话时长 |
计费主要是通过Duration计算出来的 |
Calling |
主叫 |
电话的拨打方 |
Called |
被叫 |
电话的接听方 |
Resource |
资源 |
套餐背后的权益(免费通话时间,亲情号码)被统一抽象为可以消耗的资源。 |
这里,大家需要注意的是除了显示概念(即需求里有明确说明的。比如账户这个概念),还有隐式概念。隐士概念有时候隐藏地很深,不能轻易的获得。比如上面标红的Resource这个概念,就是一个非常重要的隐式概念,是我们对套餐背后隐藏的客户权益进行的抽象,因为这个权益是可以被消耗的,所以管它叫资源(Resource)。
3. 两个Tips
在建立领域词汇表的时候,有两个点需要注意一下。
一个是语言是符号,共识即正确。领域词汇表不是某个人的,而是团队的重要共识和资产。它首先需要团队达成共识,容易理解,至于翻译的正确性反而次之。我经常拿Kangaroo(袋鼠)这个单词举例子,库克船长初到澳大利亚的时候,指着袋鼠问土著这是什么,土著很害怕,回答说Kangaroo。其本意是说“我不知道”,虽然 Kangaroo不是袋鼠的本意,但作为一个符号,并不影响我们理解和使用。另外,为了方便理解,我曾经在CRM系统里,把私海这个概念用典型的Chinglish——PrivateSea来表示,而不是用正统的翻译Territory,因为PrivateSea更有利于大家理解。
所以,计费系统领域词汇表也是一样,比如套餐这个词,我在上课的时候,有学员把它翻译成Combo,说实话,我是觉得比Plan更好的。但假如团队已经习惯了用Plan来表示套餐,继续沿用也没问题。
第二个是语言会发展,也会迭代。鉴于统一语言的核心地位,领域词汇表就像是“圣旨”,大家都应该严格遵循。然而哪有一层不变的东西呢,有时候我们会发现新的概念,然后会补充到词汇表中,也可能发现直接的理解出现了问题,变更词汇表。当然,变更的成本是很高的,但如果非变不可的话,我们也要拥抱变化,该改就改。
二、设计一致
理解了问题域,也有了领域词汇表,接下来便可以开展我们的设计工作了。设计工作主要包括应用架构设计,领域模型设计,详细设计等。这部分的要点是我们的设计要严格和领域词汇表保持一致。这里我们暂时忽略非功能设计需求。因为在当今基础实施相对比较完善的今天,DFX挑战没有太大,主要挑战还是在业务自身。当然有时候DFX也会变得很关键,只是这里我们先不考虑。
1. 应用架构一致
在进行设计之前,我们首先要确定一下应用架构。关于应用架构,这里就不过多着笔了,我其它文章有详细阐述。总体来说我们要遵循整洁架构(Clean Architecture)的思想,具体落地,你可以使用我开源的COLA(https://github.com/alibaba/COLA/)架构。
还是以计费系统为例,假设我们使用的是COLA架构,那么整个应用的形态。
为了对应用架构进行看护,我们可以考虑使用ArchUnit工具,来守护架构的完整性,主要是分层策略和层间以来关系。对于COLA架构,我们可以使用下面方式进行看护。
public class CleanArchTest {@Testpublic void protect_clean_arch() {JavaClasses classes = new ClassFileImporter().withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS).importPackages("com.huawei.charging");layeredArchitecture().layer("adapter").definedBy("com.huawei.charging.adapter").layer("application").definedBy("com.huawei.charging.application").layer("domain").definedBy("com.huawei.charging.domain").layer("infrastructure").definedBy("com.huawei.charging.infrastructure").whereLayer("adapter").mayNotBeAccessedByAnyLayer().whereLayer("domain").mayOnlyBeAccessedByLayers("application", "infrastructure").as("The layer dependencies must be respected").because("we must follow the Clean Architecture principle").check(classes);}
}
2. 模型设计一致
确定了应用架构,我们可以开始我们的建模工作了,这里的模型主要是领域模型(或者叫概念模型),包括领域建模和边界划分。补充说明一下,和计费系统关联的还有通话控制系统和账户系统。
这里我们主要关注的是计费系统,对于计费系统而言,它的核心实体无外乎就是Account、ChargePlan、ChargeRule、ChargeRecord等这些,其领域模型如下图所示。其中Account虽然是账户系统中的概念,但对于计费系统也同样重要,所以也应该表达出来。
再复杂的领域,其核心领域模型都不会太复杂,这种图可以用类UML类图来画,但是不要暴露太多的细节。另外,模型中的概念要严格遵循领域词汇表中的定义。如果在设计的时候发现词汇表需要调整,可以返回去修改词汇表。但千万不要分叉,这是保证我们设计一致性的第一关。
3. 详细设计一致
领域模型表述了系统的核心概念,但是要落到代码上,还是需要进一步进行详细设计。对于计费系统来说,计费是对账户(Account)的计费,一个账户可以开通多个套餐,每个套餐都拥有一个和多个计费规则等等。基于这样的思路,我们可以设计更加完整的类图,来指导我们写代码。
这是对领域模型的细化,添加了代码实现中需要的一些新元素,比如ChargeContext是用来做Charge的上下文,CompositeChargeRule是对计费规则进行了组合模式设计,对计费规则进行了封装,简化了Account的计费计算。
除了详细的类图设计,有时候对于复杂的业务逻辑,我们也会借助泳道、流程图、状态图、时序图等帮助我们理清业务逻辑。比如想下面这个泳道图实际上是在表达不同套餐之间的优先级关系,很显然家庭套餐优先级最高。
这里要再次强调一下,详细设计和模型设计一样,也要和领域词汇表保持一致。词汇表是圣旨,你可以调整,但不能偏离。
三、代码一致
最后,也是最关键的部分。就是我们的代码实现要如何和我们的设计保持一致。首先,让我们先看一下计费系统(charging)的应用架构代码,这里最外层的4个package正是我们COLA提倡的4个层次。
如果去查看应用的DSM(Dependency Structure Matrix,依赖结构矩阵),会得到如下的分析结果,说明Domain的确是应用的核心,因为Domain被Application依赖了69次,被Infrastructure依赖了19次,而Domain自己没有对其他层次的依赖,这是符合我们架构约束要求的。
在保证应用架构一致性的前提下,我们还需要确保我们的设计和代码也是一致的。不妨,让我们整体上看一下我们Domain层的代码。
上图的代码结构说明,在domain里面最要的是account和charge,其中charge里面有chargeplan和chargerule,其中的每一个概念,每一个类都是和我们领域词汇表、设计中的命名保持一致的。这和我们的模型设计和详细设计是一致的。
语言的一致性是基础,对于代码来说,正确的反映模型关系,正确的反映设计意图也非常重要,否则,即使你做到了语言一致,也不算设计和实现一致。在设计部分,我说过计费是针对Account进行的,而且一个Account可以开通多个套餐,那么,我们不妨来看看Account的代码:
public class Account {/*** 用户号码*/private long phoneNo;/*** 账户余额*/private Money remaining;/*** 账户所拥有的套餐*/private List<ChargePlan> chargePlanList = new ArrayList<>();;@Resourceprivate AccountGateway accountGateway;public Account(long phoneNo, Money amount, List<ChargePlan> chargePlanList){this.phoneNo = phoneNo;this.remaining = amount;this.chargePlanList = chargePlanList;}/*** 检查账户余额是否足够*/public void checkRemaining() {if (remaining.isLessThan(Money.of(0))) {throw BizException.of(this.phoneNo + " has insufficient amount");}}/*** 对账户进行计费*/public List<ChargeRecord> charge(ChargeContext ctx) {CompositeChargeRule compositeChargeRule = ChargeRuleFactory.get(chargePlanList);List<ChargeRecord> chargeRecords = compositeChargeRule.doCharge(ctx);log.debug("Charges: "+ chargeRecords);//跟新账户系统accountGateway.sync(phoneNo, chargeRecords);return chargeRecords;}
}
从代码中不难看出,整个计费的入口是在Account的charge(ChargeContext ctx),Account的chargePlanList属性是其拥有的套餐。这个和我们设计中的语义是完全一致的。
再比如FamilyChargePlan(家庭套餐)这个类,前面我们说过套餐背后隐含的是资源(Resource),这个就是通过ChargePlan的getResource( )来体现的。
public class FamilyChargePlan extends ChargePlan<FamilyChargePlan.FamilyMember> {public FamilyChargePlan() {this.priority = 2;}@Overridepublic FamilyMember getResource() {return new FamilyMember();}public static class FamilyMember implements Resource{private Set<Long> familyMembers = new HashSet<>();/*** Mock here, 真实场景,情亲号码需要从外系统获取的*/public FamilyMember() {familyMembers.add(13681874561L);familyMembers.add(15921582125L);}public boolean isMember(long phoneNo) {return familyMembers.contains(phoneNo);}}
}
所以,在落地代码环节,这里的关键主要是两个层次的一致性:
第一层:代码中的语言要和统一语言保持一致,这个是基础,这一点做不到,设计和实现肯定不会一致。
第二层:代码的实现要体现设计意图,也就是你设计中的模型和代码要对应起来,设计中的关系(继承、组合、依赖等)在代码中要不折不扣的表征出来。
四、总结
为了保证设计和实现的一致,统一语言是关键。只有在统一语言的牵引下,把我们从沟通、文档、设计、代码中的领域概念对齐,并在此基础上,最大程度的保证我们的代码反映出我们的设计意图,才有可能减少设计和实现之间的损耗。
完整计费系统代码案例:
https://github.com/alibaba/COLA/tree/master/samples/charge
如果文章对你有帮助,请支持我的新书:
如何减少软件设计和实现之间鸿沟相关推荐
- 【翻译】NIST IR 8151: 显著减少软件漏洞——致美国白宫科技政策办公室
原始文章来自https://hardenedlinux.github.io/system-security/2019/07/05/NIST-IR-8151.html,翻译很生涩,但是文章内容是切中要害 ...
- 在软件设计前先画界面图
hennry注: 需求<-同时做->界面(html/css) <--> 开发 不过最好用"快速后台数据"填充界面 画出全部(几乎)界面图好似不敏捷?疑问? ...
- 软件设计之 数据库设计
[按语:在软件设计或是动态网站开发中,数据库设计时很重要,我觉得可以说是开发工作的核心部分,所以学好数据库设计,是很重要的,也是大有前途的...] ◆.概念 首先要搞清楚容易混淆的两个概念:&quo ...
- 软件设计应该遵循的基本原则有哪些?
1.1)高内聚.低耦合 一个软件系统要有一个稳定的架构,不会随着需要的改变在发生巨大的变动.因此,高内聚.低耦合是一个软件系统设计中必须遵循的基本原则. 所谓高内聚,是指一个软件模块内各个元素彼此结合 ...
- 软件设计中的一些原则
本文为大家介绍软件设计中的一些原则,都是经过长期经验总结出来的知识,每一个程序员都应该了解,相信对大家在进行软件设计的过程中会有很大帮助. Don't Repeat Yourself (DRY) DR ...
- 一些软件设计的原则【转】——本来想自己总结,结果发现个更全的,
本文为大家介绍软件设计中的一些原则,都是经过长期经验总结出来的知识,每一个程序员都应该了解,相信对大家在进行软件设计的过程中会有很大帮助. Don't Repeat Yourself (DRY) D ...
- 重拾面向对象软件设计
简介:从上个世纪五十年代冯·诺依曼创造第一台计算机开始,一直到现在只有短短70年时间,从第一门计算机语言FORTRAN,到现在我们常用的C++,JAVA,PYTHON等,计算机语言的演进速度远超我们所 ...
- visual studio 设计器不显示_面向国际市场的装置开发运维软件设计与实现
南京南瑞继保电气有限公司的研究人员陈宏君.张磊.徐睿.曾凯.刘坤,在2019年第3期<电气技术>上撰文,分析了面向国际市场的用户软件现状与问题,介绍了新一代控制保护平台PCS-S系列装置配 ...
- 如何取得好的软件设计
[转贴] 段先德 2006-5-20 似乎作为一个软件开发者,就注定要背着沉重的行囊,穿行在茂密的热带丛林里,酷热,没有风,只有腐烂的植被.浓浓的瘴气.不时从肩膀上爬过的毒蜘蛛和从脚背上" ...
最新文章
- YOLOv3和YOLOv4长篇核心综述(下)
- Object.prototype.toString.call()检测
- 一分钟了解Android横竖屏 mdpi hdpi xhdpi xxhdpi xxxhdpi
- java magic number_避免JDBC查询中的CheckStyle magic number错误
- LINQ 学习路程 -- 查询语法 LINQ Query Syntax
- cpython教程_python高性能扩展工具-cython教程1快速入门
- imp导入dmp文件报:IMP-00038: 无法转换为环境字符集句柄IMP-00000: 未成功终止导入
- Jsp+Ssh+Mysql+Redis实现的Java Web订餐点餐
- redmine 和 gitolite 的整合
- 计算机怎么放映文档,如何从Apple TV上的计算机播放视频文件
- unity中计算三角形的外接圆
- 洛谷 P3388 【模板】割点(割顶) 根+非根+dfn[]+low[]+不一样的Tarjan算法
- Free校园小程序 开源发布,一款集合表白墙、失物招领、兼职和闲置二手买卖的云开发微信小程序
- php 调取百度天气api
- python数据可视化学习
- VMware workstation的三种网络模式
- 借助“商业模式画布”探索产品的用户需求与价值主张
- linux 烧片文件生成,在Linux上烧录CD
- JAVA毕设项目校园跳蚤市场(java+VUE+Mybatis+Maven+Mysql)
- STM32实现定时器控制LED闪烁