Skip to content
On this page

第6章 分析工作流理论

← 返回《软件方法》

从分析工作流开始,我们每个内容都分为两章。一章讲述建模知识, 一章讲述建模知识如何应用在本书案例中。这样的分割主要考虑到更符 合实际的工作。 例如,在讲解分析类图时,我们讲解知识的顺序是这样的:

  • (1)识别类和属性
  • (2)审查类和属性
  • (3)识别类之间的泛化
  • (4)识别类之间的关联

如果把案例剖析分解到每个知识点,为了让案例的剖析符合内容的 顺序,可能就会出现这样的情况: 讲解完识别类和属性后,案例剖析时,先列出很多类和属性,但没 有泛化和关联关系,因为类的关系还没有讲到,所以即使观察到,也故 意不画上去;接下来,讲解完类的关系后,案例剖析时,再把关系加上。 这不符合实际工作中的情况。实际工作中,以上列出的几项工作看 起来是交叉进行的。 我们在识别类和属性的过程中,可能会发现一些类有相同的属性, 于是泛化出超类,可能会发现有的属性还可以在同一领域内分解,于是 识别出另一个类以及两个类之间的关联……

注意我的用词:【看起来】交叉进行,因为更微小的过程依然遵守 上述的推导过程(1)→(2)→(3)(4)。

为了避免造成误解,我们先完整地讲解知识部分,本书案例如果有 和所讲解知识相关的内容,会随时引用。然后在案例部分,按照实际工 作中的思考方式灵活应用前面所讲解的知识点。

8.1 分析工作流概述

8.1.1 知识的表达和组织

在业务建模和需求工作流,我们一直把目标系统看作是一个整体, 想办法推导出涉众在意的整体表现,即系统的需求。

系统为了满足需求,必须封装一定的知识。这些知识不会乖乖地自 己从知识的海洋中走出来,而是需要软件开发人员一点点识别出来,并 有组织、有次序地把它们封装到系统中。

因此,我们要思考:

  • (1)如何准确表达系统需要封装的知识,让系统满足需求;
  • (2)如何合理组织系统这些知识,低成本地让系统满足需求。

如果不能合理组织知识,当新需求到来时,准确表达知识的成本也 会越来越高。如果考虑到利润,很难停留在(1)而不追求(2)。

不管是纯粹在大脑里面打转转,还是借助了纸笔或建模工具来协助, 以上的思考都是逃不掉的。

如果需要封装的逻辑很简单,人脑的容量和运算速度能够胜任,在 大脑里打转转还可以勉强应付,但是,能带来利润的系统都是复杂的(参 见第 1 章),借助纸笔或建模工具来显式表达思考的过程很有必要,毕 竟大脑容量和运算速度比一般人高出一个数量级的天才是很稀罕的。

注意区分“在大脑里完成”和“拍脑袋”的区别。 有的人故意不显式表达,声称“大脑思考就够了”,背后的真相可 能不是天才而是遮羞——你让他显式表达,他也表达不出来,因为他 没有掌握准确表达知识和合理组织知识的方法。

就像考试一样,面对一道有难度的填空题,考生可能有以下三种表 现:

学霸:在大脑中迅速完成推导,直接在答卷填写结果。

普生:拿出草稿纸,规规矩矩推导,然后在答卷填写结果。

学渣:拍脑袋蒙一个,直接在答卷填写结果“敏捷试错”。

因为学霸的“在大脑中推导”和学渣的“拍脑袋”有一个容易观察 到的共同点:没有草稿纸,所以学渣有时候会用这个共同点来假装自己 是学霸,这一点要警惕。

8.1.2 核心域和非核心域

一个信息系统封装了若干领域的知识。其中,有一个领域的知识是 该系统不能抛弃或替换的,这个领域称为"核心域",其他领域称为"非 核心域"。

系统类型核心域概念非核心域概念(选择之一)
文档处理器文档、页、行、字……CStringArrayCFileDialogMSXML……
电子商务网站商品、订单、会员……</div>ActionFormSessionFactory……
操作系统处理器、内存、页面……structcharint

以文档处理器为例,开发 Microsoft Word 和 LibreOffice Writer 所使用的编程语言和组件不一样,但文档、页、行、字等核心域概念是 一样的。即使回到计算机诞生之前或者去到未来,这些概念也依然存在。

还需要注意的是,核心域既可以是非计算机领域,也可以是计算机 领域。例如操作系统,其核心域概念有“处理器”、“内存”等。

关于“核心域”和“非核心域”,一种常用的通俗说法是"业务"和" 技术",但"业务"和"技术"的说法不严谨。 有的开发人员在潜意识里用“懂”、“感兴趣”来划分"业务"和"技 术":

  • *我懂且我感兴趣的知识→技术;(我懂 Java 编码,我对 Java 编码感兴趣,Java 编码是技术)

  • *我懂但不感兴趣的知识→业务;(下单、收银、配送我懂一些,但不感兴趣,这些是业务)

  • *我不懂但感兴趣的知识→高科技;(我不懂深度学习,但很感兴趣,哇塞,高科技)

  • *我不懂且不感兴趣的东东→忽悠。(我不懂 UML 建模,也不感兴趣,妈的,忽悠)

有的开发人员在潜意识里则用“是否和计算机有关”来划分"业务" 和"技术":

*和计算机无关→业务;

*和计算机有关→技术;

本书中的“核心域”和 Eric Evans 以及 DDD(领域驱动设计)话 语体系中的“核心域”(Core Domain)意思不同。

本书中的“核心域”指信息系统中不可替换的那部分内容——这 个以软件开发人员的知识是可以判断的。

DDD 话语体系中,把“领域”(相当于本书中的“核心域”)划分 为"核心域"、“通用子域”、“支撑子域”等,例如“Delivery”是核心, “Customer”是通用,“Billing”是支撑——这个划分已经超出了软 件开发人员的知识。

一家商场之所以能击败其他对手,原因未必是下单环节有什么不同, 倒有可能是在配送环节下了大力气,或者支持的支付渠道多,或者客户 服务环节抓得好。没有经过商业竞争的思考,武断地认为某个子领域是 系统的“核心”是不合适的。

软件开发人员没有能力和责任做出商业竞争方面的判断。他要做的 是,把涉众关于商业竞争方面的思考如实表达,并且用信息系统来封装 其中适合封装的部分。

如果发现存在这样的“软件开发人员”,他有责任做出商业竞争方 面的判断,那么说明这个人扮演了多个角色,既扮演涉众,也扮演软件 开发人员。

另一个可以选择的用词是“问题域(Problem Domain)”,这也 是之前大多数面向对象方法学的用语。但近年来, “问题域”和“解决 方案域”等用语被领域驱动设计伪创新赶时髦胡乱使用,已经被严重污 染。经过考虑,本书使用用语“核心域”和“非核心域”。

8.1.3 域之间映射和协作的套路

我们看一个人员管理系统的核心域类图,如图 8-2 所示。

text
图 8-2 人员管理系统的核心域类图
@startuml
left to right direction
skinparam defaultTextAlignment center
class Organization {
    - name: string
}
class Person {
    - name: string
    + AttendWorkshop(): void
}
class City {
    - name: string
}
class ContactAddressType {
    - name: string
}
class ContactAddress {
    - address: string
}
Organization .. Person : -curOrganization
Person .. City : -curCity
ContactAddressType .. ContactAddress : 1..*
Person .. ContactAddress : 1..*
@enduml

如果将图 8-2 中的 Person 类映射为 C#实现,可能会得到图 8-3 的 C#代码:

如果将图 8-2 中的类映射到关系数据库,会得到图 8-4 所示的数 据库结构:

如 果 采 用 某 种 对 象 - 关 系 映 射 器 框 架 ( 例 如 微 软 的 Entity Framework),Person 对象和数据库中的 Person 表里的一行可能会 这样联系起来:

        person1=context.Persons.Find(ID)

注意,域和域之间的映射以及协作的套路,与域中的个体并不直接 相关。

如果将以上内容中的 Person 改成 Dog,City 改成 Cat,映射的套 路没有变化。即使我们调整了域之间的映射和协作的套路,得到的结果 也会按照我们的调整有规律地变化,与域中的个体依然无关。

平时我们看到的一些“架构” ,就是域之间映射和协作的一些套路。 图 8-5 列出了现在常被提起的一些“架构”,可能在很多系统中都会观 察到,即使这些系统的核心域及非核心域都有不同。

既然域之间的映射和协作有“套路”,过早地混合不同域的知识是 不划算的。

假设三个域要考虑的知识点分别是 a、b、c 个, 如果分开考虑,然后选择好域和域之间映射的套路,负担最小可以变成 a+b+c;如果混在一起考虑,大脑的负担最大会达到 a×b×c。只要 a、 b、c 稍微大一些(都大于√3),相乘的结果就会大于相加的结果,随 着 a、b、c 进一步增大,差距会迅速增大。

过早地混合不同域的知识,会加重开发人员大脑的负担,导致开发 人员腾不出脑力来思考核心域中更深刻的问题,只好稍微折腾一下 “域之间的架构”,心里安慰自己,我有“架构”了!却忘了, 其实还没有触碰到最需要大脑去思考的核心域概念和逻辑。而这又很可 能会被巧妙地当成遮羞布——不是我不思考,而是要想的事情太多了 顾不过来啊!

而这种微妙心态的进一步发展,会导致开发人员有意无意地混合不 同域的知识,把复杂度弄成 a×b×c,以此达成废话刷工作量——以最 少的思考得到最多的“成果”。

近年最时髦的就是借 DDD 话语体系刷工作量了。例如,刚找出一 个类 Order,然后周围就围上一圈 OrderFactory、OrderRepository、 OrderService……,洋洋得意地把工作量刷了好几倍。

我经常听软件组织的架构师向我介绍他们所开发系统的“架构”, 口沫横飞,说的基本上都是“域之间的架构”。好啊,真棒, 我知道了。还有呢?没了?

构思那些“域之间的架构”是某些基础设施厂商或者方法学家的工 作,我们挑一个适合自己项目的套路用上就行了。有什么问题,可以去 请教用这个套路用得好的先行者或者与其合作。

“域内部的架构”,那些核心域概念和复杂逻辑,这是系统最值钱 的地方。要是我们没有办法理清楚,别人是帮不到我们的。这才是大脑 最该用的地方!

8.1.4 被忽视的核心域

8.1.4.1 鸟类里充兽,兽类里充鸟

当今的软件开发现实,核心域受到的重视远远不够。 我们经常会看到这样的场景: 开发人员张三喜欢研究“底层”。明明他的本职工作是用 C#编写 生产管理系统,却不好好思考如何吃透核心域把项目做好,而是花时间 研究编译器、操作系统甚至硬件。虽然工作是耽搁了,张三给人留下“勤 奋好钻研”的印象。

甚至张三还可以写博客或公众号,在网络上成为媒体热爱的“网红 程序员”。“网红程序员”很少探讨他当前所开发系统的复杂领域逻辑, 更多是谈论某种语言或框架的特性。 张三这是在挑战高难度,勇攀科学高峰吗?非也。 脱离自己真正要去解决的问题,热衷于“钻研(其实只是学习)底 层”,这样的行为更像是偷懒而不是勤奋。所谓“底层”也只是另一个 领域的知识,那个领域自有另外的人去研究。玩票式的“钻研”,在真 正专注研究这个领域的研究者看来,实在是不值一提。

但是人性的弱点如此,正如钱钟书所说:“蝙蝠碰见鸟就充作鸟, 碰见兽就充作兽。人比蝙蝠就聪明多了。他会把蝙蝠的方法反过来施用: 在鸟类里偏要充兽,表示脚踏实地;在兽类里偏要充鸟,表示高超出世。 向武人卖弄风雅,向文人装作英雄;”。

比起研究真正需要自己研究的底层(例如质管部和生产部之间的冲 突、生产过程中的各种概念之间的关系……),研究(其实只是学习) MSIL 的语法等等,要容易和愉快得多。

开发一个基础设施领域的系统,例如操作系统,只需要关注计算机 的资源,不需要关注客户、订单、库存、病历等具体某个应用领域的概 念。也就是说,“负载”比较低。

另外,基础设施领域有大量已出版教材和先行例子,高校也为计算 机和软件相关专业学生开设了相应课程(Linus Torvalds 就是在大学 教材中 MINIX 案例的激发下编写了 Linux)。这样,开发人员的大脑 比较容易把握基础设施领域的复杂性。

在 2023 年 12 月 10 日用"操作系统"为关键字搜索当当网 (dangdang.com),得到 315132 件商品,按 1/100 来挤水分也有 3000 多件。其中,一步步教读者如何自己编写操作系统的书也不在少 数。

市场已经对此做了回答。自己写(抄)一款操作系统并不难,但写 一款能带来利润的操作系统太难了——这也同样适用于其他基础设施。

很多能够带来利润的应用系统,就没有基础设施那么好的待遇了。 “自己动手写XX应用系统”的书也不是没有,但其数量、深度和实用程 度很难和基础设施的海量资料相比。

按道理,开发一个库存系统也应该可以不管基础设施,但遗憾的是, 当前现实中大多数情况下还是要管。开发团队除了要懂得库存领域的知 识,还需要懂 MySQL、Apache、Linux……也就是说,“负载”比较高。

媒体的关注度也不够。一名上班族早上起来,先上厕所用智能马桶 冲 PP,然后用智能电动牙刷刷牙,用微波炉热牛奶,边刷抖音边吃早 餐,坐电梯下楼,开车去公司,用公司的业务系统工作。上面涉及到的 七个智能系统中,估计只有抖音的开发人员可能会引起媒体的兴趣。

经常有"知名程序员"在谈到对建模(其实是 ABC 工作流)和 UML 的看法时,表示"对我来说不重要",进一步探究会发现,这些"知名程 序员"要么其所开发的系统是基础设施领域的系统,要么根本没做好自 己的本职工作。不管是哪一种情况,这些“知名程序员”是知道谈论哪 些内容更容易撩拨媒体从业人员的兴奋点的。

市场经济中,不存在哪个领域比其他领域更核心。如果像过去"以 粮为纲"、"以钢为纲"一样,扭曲市场信号,硬性指定某个领域(芯片、 操作系统)更核高基,造就的多半是骗取纳税人金钱的投机分子。

8.1.5 要重视分析工作流

分析,就是从核心域的视角构思系统的内部机理。

在现在的很多软件组织中,分析工作流的技能被严重忽视。很多开 发人员上手就直接编码,原因并不是软件开发项目的核心域逻辑极其简 单,不需要分析,或者他的大脑极其发达,在大脑里就可以完成分析, 而是开发人员缺乏分析的技能,只好草草跳过这一步。

为了遮掩自己的无能,开发人员还会使用各种遮羞布——引入各 种核心域逻辑之外的因素把水搅浑。 遮羞布一:时间

以“时间紧”、“敏捷”为借口,掩盖自己没有能力剖析复杂逻辑的 事实——我是有能力剖析的,但时间太紧张了,等以后有时间吧!

遮羞布二:空间

借助“口头交流”、“白板”等容量小的介质,掩盖自己没有能力剖 析复杂逻辑的事实——我是有能力剖析的,但白板空间太小了,只好 简单画个“草图”了!

遮羞布三:功能需求之外的其他需求或设计因素 在思考核心域逻辑时,频频提出“这样会不会速度慢”(质量需求)、 “我们想把它分成 N 个微服务,让不同团队用各自技术栈开发”(设计 约束,也有可能是臆想的设计)等和核心域逻辑无关的因素,掩盖自己 没有能力剖析复杂逻辑的事实——我是有能力剖析的,但还要考虑到 这个因素、那个因素,所以精力就不够了。

遮羞布四:重构 以“后面再重构”为借口,结合其他遮羞布,掩盖自己没有能力剖 析复杂逻辑的事实——我先随便写写,后面再重构,哎呀,没想到啊, 时间来不及了(遮羞布一)。

关于重构,此处多说两句。

上世纪80年代末,BilOpdyke( http://laputan.org/pub/papers/opdyke-thesis.pdf ) 和Bill Griswold(https://cseweb.ucsd.edu/~wgg/Abstracts/gristhesis.pdf)等人 归纳了一些调整代码结构的手法,称为“重构”,后经 Martin Fowler 等人推广而广为流传。

“重构”的知识可以看作是建模知识的一个子集。如果开发人员真 的熟练掌握重构的手法,很多情况下他已经有能力直接建模系统的核心 域逻辑得到更合理的结构,根本不需要先走很多弯路再回正路。

要是开发人员以“重构”为理由拒绝思考,很可能他的所谓“重构” 也是空话。

摸着石头过河是难免的,但应该在不得不摸的时候才摸,不应该假 装看不见已有的路和桥,无论大小事都主动追求摸着石头过河。

当然,也有的人不是假装看不见路,而是真的看不见路——就是 个睁眼瞎。不过,大脑不用思考,凭感觉摸着石头过河不停刷工作量, 也是一种躺平的幸福。

用考试类比

学渣参加考试时,会这样遮掩自己的无能:

遮羞布一(时间):抱怨时间紧张或者故意提前交卷——如果再给 我一些时间,我肯定做得出来。

遮羞布二(空间):故意带不好写的笔或抱怨草稿纸质量差、答题 的地方太小——不是我不想好好答,可惜这个纸笔不给力。

遮羞布三(其他因素):抱怨和学科知识无关的其他因素,例如要 求用仿宋体答题——不是我不会,写仿宋体耗费了我很多精力。

遮羞布四(重构):我先随便答,一会回来再检查。(结合遮羞布一) 哎呀,来不及检查了。结合遮羞布二)哎呀,答卷上地方不够了。

8.1.6 分析方法学历史的简单回顾

1958 年,John W. Young Jr.和 Henry K. Kent 发表“Abstract formulation of data processing problems”,第一次提出在独立于 实现的抽象级别上定义系统的规范。

1959 年,CODASYL(数据系统语言会议)成立。1962 年, CODASYL 提出了一个和 Young/Kent 类似的模型,称为“信息代数” (Information Algebra)。

1970-1980 年代是结构化分析方法的时代,主要贡献者有 Börje Langefors、Chris Gane、Trish Sarson、Tom DeMarco、Pin-Shan Chen、E. F. Codd 等人。结构化分析的主要建模方法是数据流图和实 体-关系图,这两者的结合,让软件开发人员有能力剖析大型系统。

1982 年,Nastec 公司开发出了 DesignAid,这是第一款 CASE (计算机辅助软件工程)工具。随后,其他 CASE 工具陆续出现。据 PC Magazine 的 1990 年 1 月 30 刊统计,当时已经有超过 100 家公 司提供了将近 200 款 CASE 工具。

1980 年代后期,面向对象的思想开始用于分析和设计。然后,UML 统一了表示法。这部分历史已经在本书第 1 章“UML 简史”部分讲述, 此处不再赘述。

8.1.7 本书使用的分析方法

分析模型描述系统要封装的核心域知识。 用什么建模概念来思考和描述核心域知识,可以有很多种选择。例 如,“人”用不同的建模概念描述,可以说它是一个“类”,也可以说它 是一个“类型”、一个“实体”。

本书使用面向对象的建模概念来描述分析模型,从三个视角来描述:

分析类模型:描述系统中各个类以及类的关系,如果用 UML 的类 图表示,则为分析类图。

分析状态机模型:描述某个类的各个行为的逻辑,如果用 UML 的 状态机图表示,则为分析状态机图。

分析交互模型:描述某些类在实现某个用例时的协作,如果用 UML 的序列图表示,则为分析序列图。

需要说明的是,虽然我们用的是面向对象的分析方法,也就是说, 用面向对象的概念来剖析核心域知识,但不意味着你的系统一定要用特 定的“面向对象”编程语言、特定的存储方式或物理分布形式来实现。

也许你使用的编程语言是面向过程语言,例如 C;也许你使用的编 程语言是函数式语言,例如 F#;也许你使用的存储系统是关系数据库 系统,例如 SQL Server;也许你使用的存储系统是非关系数据库系统, 例如 MongoDB;也许你的系统运行在同一台机器上,也许是分布在 很多台机器上……

不管你的系统的实现方式和运行形态如何,从分析过渡到设计时, 变化的只是分析到设计的映射套路。如果设计所使用的非核心域比较 “面向对象”,那么映射套路会比较直观一些,否则,就需要一定的转 换。但无论如何,如前文所说,这个套路和具体的核心域知识没有关系, 我们并不需要针对每一个核心域概念逐一花费脑力去思考它。

我们之所以选择在分析工作流使用面向对象的分析方法,是因为从 思考深度和表示的严谨程度来看,面向对象的分析方法以及 UML 表示 法目前仍然是剖析和整理核心域逻辑的最佳选择。

本书在设计工作流的内容,会展示分析模型和各种实现方式的映射 套路。