仓库源文

.. Kenneth Lee 版权所有 2016-2020

:Authors: Kenneth Lee :Version: 1.0

两种基本的构架表述方法


DFD方法

在这个专栏的前面,我们花了很多时间讨论架构的战略问题,因为对于架构这么高层的抽 象,如果我们直接进入具体方法,我们很容易被具体的方法所左右,忘掉这个设计本来的 目的。而对于高层抽象来说,忘掉原始目标所有努力都等于0。我以前解释《道德经》的时 候,经常说,道德经是做“大事”的战略,不是用来解决具体问题的,很多人不以为然,甚 至会举一些生活的小例子,证明“这个不是也和道德经”一样的吗?拜托,这个东西一样和 解决问题有一毛钱关系吗?你非要用牛顿定律来判断“放手,手上的球就会掉到地上”的吗 ?你到底要解决问题还是要耍嘴皮子?

从屋里走到屋外是不需要战略支持的,从深圳去洛杉矶才需要战略支持,因为你不是直接 用直觉走的,而且基于你的知识,知道要先过关去香港,然后从香港飞三番市,最后才转 机去洛杉矶的(当然不是只有一条路,我只是举个例子)。这种情况才需要战略。否则你 到了香港,一转眼跑到旺角去买奶粉,然后跑回深圳高兴地对亲戚说,你看,我带了三罐 奶粉回来,没被人逮到,我好厉害哦。这就很没有意思了,对吧:)

各种架构表述方法,它们的核心也只有在对准目标才有其意义,如果对不准目标,DFD就只 是一张不痛不痒的图,Class Diagram也就只是代码不如的重复,没有意义的。

但另一方面,DFD和Class Diagram都是非常重要的架构表述工具,在整个架构设计中起举 足轻重的作用。这也需要我们对准要解决问题的核心,我们才能欣赏。

以下的两篇博文中,我会分别介绍DFD和Class Diagram两种描述方法在整个构架表述中的 意义。我希望读者最终可以:

  1. 明白,这两者其实是在解决一个问题的两个方面

  2. 理解,即使你的架构设计中完全不使用这两种图,它们都是整个构架设计的承载。是你 所有其他架构表述方法两个最基本的概念模型

本篇介绍DFD方法。(注意:这里不是介绍DFD的入门知识,要了解这个你自己找书看,这 个文档是介绍DFD方法的要领)

DFD方法是大部分SASD(结构化分析结构化设计)的基本设计元素。DFD方法基于一个朴素 的事实:所有的变化中,算法是最不可能变化的要素。

架构师(或者说设计师)其实都是受虐狂,他们喜欢被限制。因为限制意味着构架分析工 作的降低。一幅画画得差不多了,让你点睛,你就得到活灵活现的龙了,但如果给你一张 白纸,你就不敢下笔。为什么给你自由了,你反而难受了呢?

你要给某个机器做一个BCD编码器,你首先要选择用什么编程语言,调研做这种东西,C的 效率高,还是Python的效率高,要不要用函数式编程,用erlang来编?要分析哪个是最好 的,你是需要花时间,说不定某个语言你不熟悉,还要去学习。这时,有人过来告诉你一 个消息:不用想这么多了,这个破平台上只有C,其他语言都不支持。——我靠,你终于松一 口气了,只有C一个选择,那就不要浪费时间了,赶紧考虑argv和argc中放什么参数是正道 。

所以,如果我们有目的的时候,自由很大程度是负担,是工作量,是束缚。

所以,只有程序员才明白什么叫自由,什么是负担。否则为什么王小波同学是自由骑士? 为什么在《万寿寺》中他需要一再改变他的故事,寻找心中“诗意的人生”,又为什么诗意 的人生反而是和SM一样的约束的人生?又为什么当所有的细节归于清晰,一切都将归于庸 俗?这些都是一个构架到细化的过程,这一点我们以后慢慢来讨论。

我们用一个例子来延续我们的讨论,比如有人找你做一个mp4的播放器,不限定方法,不限 定硬件,不限定接口,那么首先你的约束是什么?——显然,是这个事情本身的逻辑。我把 这个逻辑表述如下:

    .. figure:: _static/dfd方法1.png

这就是DFD图,它表述了这件到底是件什么事。我也不管这个是个纯软件还是硬件,也不管 你选择了什么语言来开发,也不管你的mp4是放在磁盘中的文件,还是磁带机中的片段,或 者是卫星上送下来的信号。音视频分离这件事都是少不了的,音频解码也是少不了的,视 频解码也是少不了的。我也不管你在做视频解码的时候是有状态的还是无状态的,反正这 些事情本身,就是你的最小约束。当你的需求清晰了,这个最小约束也是清晰的。

DFD表述计算机拥有的唯一能力:数据加工。把一个形态的数据变化为另一个形态的数据。 当我们进行实现的时候,实际上我们需要和物理世界发生关联,所以,在这个约束之外, 我们才为自己加上更多的约束,首先可能是为这个系统增加数模转化的部分(这已经是在 设计了,设计是人为增加约束,因为数模转换是一种信息承载方式的选择,而不是原始的 数据加工要求)。

很多DFD的设计者非常在乎这种虚拟世界和物理世界的界限。因为虚拟世界是意欲(需求) 的唯一限制,是“诗意的世界”。而物理世界是无可奈何的选择,是我们选择的约束。分清 楚这两者,有助于我们判断为我们自己增加什么约束,而这种约束,是否真正和我们我们 内心的欲望是对齐的。

DFD建模中,把增加了物理世界选择的DFD称为扩展DFD(EDFD),下面是上面这个例子的一 个EDFD的设计:

    .. figure:: _static/dfd方法2.jpg

大家也看到了,这层加工,你可以选,你可以不用文件而用tuner来输入mp4码流,但无论 如何都是在任何其他设计以前的第一个选择。所以,很多人甚至不会区分DFD还是EDFD。但 我们心中,需要非常清楚,我们在这层建模中到底在干什么。

建模的核心功能不是设计,而是选择。这是我们使用这些工具最容易陷入的误区(认为建 模的目的是描述设计)。我们不是要尝试在这幅图上严格表达一个播放系统是如何工作的 ,我们是要看看我们约束在哪里,决定如何建立下一层约束。

EDFD的下一层约束是模块承载。任何一个数据加工(或者数模/模数转换),都需要一个物 理实体来承载。比如基于上面这个图,我们首先知道,CPU本身处理不了数模/模数转换, 我们用声卡显卡来实现这种功能,我们可以使用的一种选择是这样的:

    .. figure:: _static/dfd方法3.jpg

但是,这不是唯一选择,看着这幅图,你知道你的约束以后,你是可以权衡的。模块底下 那个加工图(DFD)是最强的约束。而EDFD次之。模块选择更次之。所以,如果我们适当选 择声卡选得好,我们其实可以这样的:

    .. figure:: _static/dfd方法4.jpg

从这里我们就可以看到,建模的过程核心不是为了“描述”什么东西,而是为了说明在核心 诉求下,设计者做了什么样的选择,这样未来当我们遇到变化的时候,我们可以知道我们 有多高的自由度来响应这种变化。

这些图不一定要画出来,但其实我们一路进行构架决策,是基于这样的推演在进行的。这 种图,无论在纸面上,还是在脑子里,其实我们本质上都是基于这个逻辑来进行决策。

你不要以为这些图形很简单。其实你脑子不一定能反应得过来。比如,上面这个方案,只 要有这幅图,你会马上注意到它有一个致命的错误:显卡并没有通道传递时间信息给显卡 。这个同步必须做在CPU一侧。

一层层加约束,是一种非常有效控制熵增的方法。

DFD建模的另一个要注意的要领是,不要把独立的逻辑建立在一副图中。还是上面这个播放 系统,如果我们增加一个需求,比如字幕,这个功能其实和原始功能相对独立的,我们没 有必要建模在同一个图形中,这就是我们前面谈过的,我们要让整个设计和代码变得立体 ,立体的方法是每个独立的“方面”,应该独立“建模”。而它们之间的关联,可以放到模块 图中(SASD方法有AFD和AID都可以解决这些问题)。因为模块的增长是有限的。

还是以上面这个模块为例,假设我们采纳了第二个方案(忽略那个时间同步的错误),我 们就可以有这样一副模块图(AID):

    .. figure:: _static/dfd方法5.png

这时你做另一个建模(比如字幕):

    .. figure:: _static/dfd方法6.jpg

它并不会在你的AID中增加更多的模块。只是为每个模块提出了更多的需求而已。

只要你对其中几个最关键的需求做DFD,你的AID就会变得足够复杂,而我们知道复杂的关 系就已经是设计了,因为你已经没有余地了,构架的工作就结束了。

到那个时候,AID上的功夫,就变成Class Diagram上的功夫了。这我们在下一个博文中解 释。但读者应该至少知道,无论你走到哪里,DFD都是你的原始约束,当你开始要突破这些 约束,终究你还是要回来面对你的基本约束问题的。

Class Diagram方法

我们在上一篇中介绍了SASD的基本方法。和UML方法从故事开始分析问题不同,SASD是从一 个非常自由的角度入题的。SASD方法认为你有很多选择,所以你不知道什么样的选择是最 优的,所以你需要密切关注你的核心需求,基于这个核心需求推演你的模块分解。而UML方 法很多时候,认为你是在一个相对成熟的平台上解决问题的,比如你做一个Web Server, 很多时候我们不是用.Net,Node.js或者就是LAMP一类的方案,而且考虑到团队经验等要素 ,你的模块基本模型大体上就定了,需求扭不过现实(主要是扭不过工作量),你不需要 从DFD推演你的AID,实际是你一开始就有一个基础的AID,然后你甚至不是在开发整个系统 ,你只是部署人力“修正”这个系统,让它满足你的要求。

比如我就接过这样的需求:“降低LAMP系统在高压力下的Latency”。这个模型需要的是和架 构设计完全不同的模型和分析方法。比如你需要定义数据流,定义Latency的概念本身,然 后定义时间模型等。但从软件构架的角度,这些是具体的分析工作,不是构架设计关心的 范围。构架设计关心的范围仍然是:代码修改在哪里?如何控制熵增?——你可以有很多办 法发现性能瓶颈,提升性能,但你动笔改接口,改模块,你就需要控制软件结构的发展方 向,这个时候,我们就需要暴露出软件的结构,并决定修改在什么地方。这才是构架关心 的范围。

对于这种系统设计已经相对完善的情形(比如SADS方法的后期,或者对现有系统的改进) ,我们谈我们如何修改这个系统的时候,我们需要对现有的模块的组成建模,这时Class Diagram就起主要的作用了,因为它是一种增强型的AID。

AID描述的问题是,系统有多少模块,模块之间的关系是什么。

还是用上一篇的解码器的例子,它的AID可能是这样的:

    .. figure:: _static/uml方法1.png

这种东西,我们用Class Diagram可以有一样的表达能力:

    .. figure:: _static/uml方法2.png

我从这个角度来入题,是请读者注意,类图不是用来给面向对象语言复原语言描述的,我 知道业界有不少把代码和类图互相转换的努力,但我认为这些努力都已经失败了。类图的 精确度永远都达不成编程语言的那个程度,硬向编程语言靠,它是不会成功的。我一个类 有20个函数,在文本文件中可以很清晰地看出来,你让我在类图上看?开什么玩笑?编程 语言还可以清晰表达宏,关联表,数组这样的细节关系,你用图来做类似表达看看?

一个工具,该干什么就干什么,非要把它放到不适合它的位置上试图“降低工作量”都是没 有意义的。

类图,是AID的发展,比单纯的AID有更强的表达力,比如上面这个构架,我可以发展成这 样的:

    .. figure:: _static/uml方法3.png

如果你细细看这个图,对比AID的表现力,你会发现类图几乎把图可以表达的最重要的要素 都展现出来了。

用这种图我们可以很容易地,相对精确地用于和大部分人探讨:我到底想怎么做这个系统 。

如果使用者可以理解类图本身可以用来表达什么,重新学一次UML的所有符号的表示方法, 就会发现,这个工具其实简单,表达能力强,让你可以完全聚焦到方案选择上。你不用再 指望从类图中挖出什么东西来了,它的基本功能已经足够好用了。其他设计有其他设计的 方法,那些不是类图关心的范围。

对于类图的使用,我只有一个提示:要记住,类图反映的是一个逻辑的名称空间,不是代 码本身。用于方便我们说到某个修改细节的时候,到底要修改什么。所以,不要指望把代 码全部反映到一个类图上。

最常见的情形是分层。比如我们基于InfiniBand(RoCEv2)做一个远程文件系统,协议站 类似这样:

    .. figure:: _static/uml方法4.png

假设这些部分都是你的设计编码的一部分,IB的每一层都需要你来实现,你也不可能把类 图画成这样的:

    .. figure:: _static/uml方法5.png

我们说了,我们提取独立的切面都是为了降低复杂度,让我们可以进行有效的设计推演和 交流,这个图把所有的复杂度一字排开,根本不能用来推演。更好的建模应该是这样的:

    .. figure:: _static/uml方法6.png

这部分的设计和IB的设计是相互独立的,不应该放到同一个名称空间中,IB协议栈本身可 以用独立的名称空间描述的:

    .. figure:: _static/uml方法7.png

这些例子,都是为了说明,类图的主要作用是用立体,分离的方法建立独立的名称空间和 角度,从而让我们有机会Applied我们的构架设计约束给下一级的优化。而不是反映代码本 身。

我觉得,架构实际的基本工具方法,掌握DFD和类图,基本上就够用了,其他都是你对那些 部件和接口本身细节的认识,不是架构方法本身可以改变的了。