在软件领域,有一个古老的神话:即我能保证设计和代码实现完全一致。这的确是一个非常有价值的目标。试想下,如果我们的系统毫无设计,或者设计和代码实现毫无关联,在当今软件如此复杂的情况下,其实现和维护难度可想而知。

本文将结合我最近给ICT做软件设计培训的一些感悟,尝试介绍一些减少设计和实现之间鸿沟的方法,这些方法包括语言一致,设计一致和代码一致。除了介绍方法之外,我会以运营商计费系统为实践案例,辅助讲解如何落地一致性设计。诚然,完美的设计和代码一致的确是神话(和人月神话一样),因此,本文的立意并非是要指导你变成“神”,而是期望在减少设计和实现不一致的征途中,能迈出一小步。

一、语言一致

语言、语言、语言”,重要的事情说三遍,我一直在不遗余力的强调语言的重要性,是因为语言是一切的基础,正如维特根斯坦所说,“语言边界决定了我们的思维边界”。

要保证设计和实现的一致,首先要保证的,就是概念的完整性和语言的一致性。如果语言都做不到一致,其他的都是扯淡。

1. 统一语言

具体而言,我们需要保证“沟通语言,文档语言,设计语言,代码语言”的一致性。我们可以通过统一语言来做到这一点,统一语言(Ubiquitous Language)这个概念来自于Eric Evans的著作《Domain Driven Design》,其本人在谈到DDD核心的时候,也总是把“统一语言”放在第一位。

形成统一语言的关键就在于我们要制定一个领域词汇表,把领域中,涉及到的核心概念,用英文、中文、解释的形式无歧义的规定下来。这个词汇表作为团队的“共识”,就是true of source,后续的活动都应该严格遵循之。

  1. 沟通语言:就是我们日常交流、会议中使用的语言,大家都应该使用领域词汇表中的语言来沟通,否则就会出现很多的鸡同鸭讲,你就需要大把的时间去澄清概念。

  2. 文档语言:不管是需求文档,设计文档,测试文档等等,里面的语言都应该和领域词汇表保持一致。

  3. 设计语言:不管是用例图、模型图、流程图、类图、时序图等等设计中,其语言也应该和领域词汇表保持一致。

  4. 代码语言:最后,也是最重要的。在工程落地的时候,我们的代码当然也要和领域词汇表保持一致。除此之外,我们的代码还要和设计意图保持一致。

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);}}
}

所以,在落地代码环节,这里的关键主要是两个层次的一致性:

  1. 第一层:代码中的语言要和统一语言保持一致,这个是基础,这一点做不到,设计和实现肯定不会一致。

  2. 第二层:代码的实现要体现设计意图,也就是你设计中的模型和代码要对应起来,设计中的关系(继承、组合、依赖等)在代码中要不折不扣的表征出来。

四、总结

了保证设计和实现的一致,统一语言是关键。只有在统一语言的牵引下,把我们从沟通、文档、设计、代码中的领域概念对齐,并在此基础上,最大程度的保证我们的代码反映出我们的设计意图,才有可能减少设计和实现之间的损耗。

完整计费系统代码案例:

https://github.com/alibaba/COLA/tree/master/samples/charge

如果文章对你有帮助,请支持我的新书:

如何减少软件设计和实现之间鸿沟相关推荐

  1. 【翻译】NIST IR 8151: 显著减少软件漏洞——致美国白宫科技政策办公室

    原始文章来自https://hardenedlinux.github.io/system-security/2019/07/05/NIST-IR-8151.html,翻译很生涩,但是文章内容是切中要害 ...

  2. 在软件设计前先画界面图

    hennry注: 需求<-同时做->界面(html/css) <-->  开发 不过最好用"快速后台数据"填充界面 画出全部(几乎)界面图好似不敏捷?疑问? ...

  3. 软件设计之 数据库设计

    [按语:在软件设计或是动态网站开发中,数据库设计时很重要,我觉得可以说是开发工作的核心部分,所以学好数据库设计,是很重要的,也是大有前途的...]  ◆.概念 首先要搞清楚容易混淆的两个概念:&quo ...

  4. 软件设计应该遵循的基本原则有哪些?

    1.1)高内聚.低耦合 一个软件系统要有一个稳定的架构,不会随着需要的改变在发生巨大的变动.因此,高内聚.低耦合是一个软件系统设计中必须遵循的基本原则. 所谓高内聚,是指一个软件模块内各个元素彼此结合 ...

  5. 软件设计中的一些原则

    本文为大家介绍软件设计中的一些原则,都是经过长期经验总结出来的知识,每一个程序员都应该了解,相信对大家在进行软件设计的过程中会有很大帮助. Don't Repeat Yourself (DRY) DR ...

  6. 一些软件设计的原则【转】——本来想自己总结,结果发现个更全的,

    本文为大家介绍软件设计中的一些原则,都是经过长期经验总结出来的知识,每一个程序员都应该了解,相信对大家在进行软件设计的过程中会有很大帮助. Don't Repeat Yourself (DRY)  D ...

  7. 重拾面向对象软件设计

    简介:从上个世纪五十年代冯·诺依曼创造第一台计算机开始,一直到现在只有短短70年时间,从第一门计算机语言FORTRAN,到现在我们常用的C++,JAVA,PYTHON等,计算机语言的演进速度远超我们所 ...

  8. visual studio 设计器不显示_面向国际市场的装置开发运维软件设计与实现

    南京南瑞继保电气有限公司的研究人员陈宏君.张磊.徐睿.曾凯.刘坤,在2019年第3期<电气技术>上撰文,分析了面向国际市场的用户软件现状与问题,介绍了新一代控制保护平台PCS-S系列装置配 ...

  9. 如何取得好的软件设计

    [转贴] 段先德  2006-5-20 似乎作为一个软件开发者,就注定要背着沉重的行囊,穿行在茂密的热带丛林里,酷热,没有风,只有腐烂的植被.浓浓的瘴气.不时从肩膀上爬过的毒蜘蛛和从脚背上" ...

最新文章

  1. YOLOv3和YOLOv4长篇核心综述(下)
  2. Object.prototype.toString.call()检测
  3. 一分钟了解Android横竖屏 mdpi hdpi xhdpi xxhdpi xxxhdpi
  4. java magic number_避免JDBC查询中的CheckStyle magic number错误
  5. LINQ 学习路程 -- 查询语法 LINQ Query Syntax
  6. cpython教程_python高性能扩展工具-cython教程1快速入门
  7. imp导入dmp文件报:IMP-00038: 无法转换为环境字符集句柄IMP-00000: 未成功终止导入
  8. Jsp+Ssh+Mysql+Redis实现的Java Web订餐点餐
  9. redmine 和 gitolite 的整合
  10. 计算机怎么放映文档,如何从Apple TV上的计算机播放视频文件
  11. unity中计算三角形的外接圆
  12. 洛谷 P3388 【模板】割点(割顶) 根+非根+dfn[]+low[]+不一样的Tarjan算法
  13. Free校园小程序 开源发布,一款集合表白墙、失物招领、兼职和闲置二手买卖的云开发微信小程序
  14. php 调取百度天气api
  15. python数据可视化学习
  16. VMware workstation的三种网络模式
  17. 借助“商业模式画布”探索产品的用户需求与价值主张
  18. linux 烧片文件生成,在Linux上烧录CD
  19. JAVA毕设项目校园跳蚤市场(java+VUE+Mybatis+Maven+Mysql)
  20. STM32实现定时器控制LED闪烁

热门文章

  1. 傅一平荐书 | 2020年春季我读过的十本好书
  2. 10个完整的Android开源项目,值得大家学习借鉴
  3. 会声会影x4素材_【自营】素材代下服务
  4. 投稿,计算机三大学报,论文,专注,专利软著,竞赛,国际会议,访学研修
  5. 关于2017房价的分析_很精辟
  6. 关于BI和AI的一点想法
  7. 什么是 CDN 缓存命中率以及如何计算和优化它?
  8. Python爬虫:批量爬取变形金刚图片,下载保存到本地。
  9. 使用Python编写淘宝抢购代码
  10. workbench工具栏缺失解决方法(仅供参考)