仓库源文

.. Kenneth Lee 版权所有 2018-2020

:Authors: Kenneth Lee :Version: 1.0

再谈“法自然”的设计思路


在上一篇论述(再谈什么是高层设计)里,我终于有机会给出“恍惚”的定义了,我们可以 从这个角度来重新,也是更加精确地定义什么是“(道)法自然”的设计思路了。

设计包括很多方面(角度),很多工程师新手觉得设计是“我决定”选择,有经验的工程师 都知道,设计是“我发现”被设计对象的特征。基于后面这个理解的设计方法,就是“法自然 ”的设计方法。

我们拿一个例子来讨论。比如我们要做一个简单的功能:统计一个文件的词频分布,更简 单起见,我们假定这是英文,没有中文断句这样的需求在里面。你会怎么分解这个系统的 模块?

基于逻辑链好看的,“我决定”的设计方法可能会这样进行设计:

分四个模块:

    A- 文件输入模块

    B- 分词模块

    C- 词频统计

    D-显示模块

这种设计方法在模块比较小的时候,常常是对的,而且,如果我们不能看进细节里面去, 也挑不出毛病。

“法自然”的设计方法不是这样的,法自然的方法首先不认为分解模块是必要的。分解模块 不是需求,是我们个人的“私欲”,分解模块不带来收益(至少在你找到之前)。所以第一 步的分解应该是只有一个模块,然后我们思考这个模块的内部流程设计:

我们用getopt就可以取得入口参数,拿到文件并打开,然后顺序读入一个个字符,把不同 单词的结果写入一个数据结构,完成统计后,基于这个数据结构把结果打印出来。

这不需要分解什么模块,但我们真正担心的问题是未来,后面我们会需要怎么扩展这个系 统?我想一种情形:

  1. 多文件支持

  2. 多文件类型支持

  3. 多种展示方式支持(比如文本打印,图标展示等)

基于前面的推演和这里对未来的期望,我们很容易就发现所有这些部分,哪里的逻辑是和 其他部分的联系是很薄弱的:扫描数据的表达形式。所以,我们需要的模块分解可能是这 样的:

    A-框架

    B-数据维护模块(支持加入数据和读取数据,支持数据格式转换)

    C-展示程序

如果我们研究出来对未来的预期不是这样的,比如,我们发现未来我们的扩展主要是近义 词合并统计,那么我们需要的模块分解可能是这样的:

    A-框架

    B-命令行语法Parser,获取近义词定义

    C-可定制Parser程序

    D-展示程序

这个例子太简单了,可能这个意思不太容易看出来,但不知道读者是否可以看出:模块的 分解不是被设计出来的,它是在把所有逻辑都摆上去后,自动因为很重的内聚和薄弱的外 部链接而“断开”的。如果我们总从自己的思路出发来控制架构,而不是抱着一种“未定”的 策略去研究细节,只会对性能和工作量造成伤害。一个系统的构架,是生成的,不是人为 扭曲出来的。

人为唯一能选择的是是否放弃那个需求,只要那个需求被接纳了,“关联”就造成了,这是 这件事情本身造就的,你无法规避,你能保证你自己不增加关联就谢天谢地了。

用图来表达,一开始我们可以这样认知这个系统:

    .. figure:: _static/系统内外逻辑1.jpg

然后我们分析它内部的逻辑:

    .. figure:: _static/系统内外逻辑2.jpg

这个关联关系是被功能驱动的,内部自然会形成一个有关联强弱的结构,这样,关联比较 弱的地方,就会断裂成外在的模块:

    .. figure:: _static/系统内外逻辑3.jpg

我这里标出一个称为“代理”的节点,这表示这个节点将成为左边的子系统和右边的子系统 的桥梁。如果我们要表演“做架构”的样子,其实任何分块都可以用代理来代替,因为我们 其实可以把代理背后的节点全部体现在代理上。但这样的结果是两个子系统的关联并没有 发生变化。我们思考这个问题,或者独立在一个概念空间中修改代码时需要面对的概念并 没有减少。

这就是我为什么常说:希望用不确定的API去取代确定的API来“解耦”其实就是装样子。因 为你丢开“程序接口”的表相,完全从概念关联的角度来想这个问题,用myapi(void * data) 来取代:::

    myapi1(struct GetServiceID *id)/myapi2(struct DoServer1 *req)/myapi3....

并没有让系统解耦(当然,这个写法本身可以成为其他抽象方法的辅助,那是另一个问题 )。因为你根本就没有改变他们之间的关联度。进了myapi内部,你还是要区分这个data本 身的内在属性。你这个动作,仅仅把关联延迟到提供myapi的模块内部而已。用上面那个图 来理解,你会制造另一个真正的断裂点。

上面只是对“法自然”策略最基本的理解:是需求在驱动我们的设计,而不是我们自己的“定 义”驱动了设计的成功。所以,所谓成功的设计,就是“不犯错”的设计,你自己不作死就很 好了,不是你引导设计的成功,而是需求在驱动你不得不做对应的选择。我们要时刻忘掉 个人的想法,让每个设计解决(至少)一个问题,而不是用“架构理念”来决定你做什么选 择。

但这个策略不限于此,如果我们理解了恍惚是什么,我们马上就面对了另一个问题:你根 据需求已经决定了一套逻辑和空间,但你还有其他逻辑需要面对,比如Latency?比如低功 耗?比如可靠性,可维护性,可服务性?可延续性?

这才是架构师面对的最难的问题,还是用前面那个词频统计的例子,假设你现在功能已经 做得相当的产品化了,所有代码100%单元测试覆盖,展现的界面请美工专门调过色, Parser里面插了对应的进度呈现算法,可以几乎线性地在界面上呈现完成的进度。

但客户不接受你这个响应速度,人家说了:我用竞争对手的产品,1G的数据只要5分钟就出 结果了,你这个,要半个小时。你怎么处理这种情况?

你做过分析,发现问题就在你的架构上,你先把结果存到磁盘上,然后才一次呈现,你的 对手是中间结果就开始呈现了,虽然取了巧,但很多用户看了个开头,心中已经有结论了 ,不需要后面继续统计,这时人家的优势就很明显了。

你觉得要解决这个问题的,好了,你原来的分解就成为你的最大障碍,特别是你在一个很 大的组织中,那些把Parser过程做到线性呈现的人可能还刚刚拿了个“2018年度XXX公司最 佳创意奖”,结果你来这一出?人家会不跟你玩的好不好?

这还是小问题,这种问题你都解决不了,你就不要当什么架构师了。但这只是一个例子, 这种例子有很多,呈现为不同的样子,它们反映了你在名称空间上面对的挑战。本质上, 架构师的很多信息是来自团队的,你自己看不完所有的细节。比如你可以要求,每处理10M 的本文,必须输出一次中间结果,但模块维护者告诉你,如果这样的话,这个模块必须重 写,而且可能需要丢失十分之一的功能。你信还是不信?你不信?自己看代码去。一个模 块可以这样,100个呢?其出弥远,其知弥少。你调查具体模块消耗的时间越多,你离开你 的构架逻辑越远。

更严重的问题是,你自己的思考也是建立在这种抽象上的,就算你自己亲手写的代码,但 写的时候是一回事,你去用它的时候,用的还是它的抽象概念(比如API)。你的思考受这 个名称空间的限制。

当你面对了这个问题,你就会发现,你只剩下一个选择,就是直接面对“恍惚”,不要尝试 去细化这些概念。简单说,你要直接基于“恍惚”去做临时决策。功能,性能,功耗,可XX 性,都是你面对的问题的一部分,让他们都呈现在你的视野中,谁先变清晰就处理谁。但 不要把这个作为你最终结论,时刻重新去找下一个“清晰”点,处理那个新的问题。这个操 作策略,就叫“法自然”。

这个说出来好像解决不了任何问题。其实不是的,它给出了另一种具体操作策略:不要认 为架构是开始写下来的那篇文档,也不要在架构设计中仅写选择。

这两个原则第一个很好理解,第二个我用一个生活的例子来解释吧。

我们常常听到有人问这样的问题:语文重要还是数学重要?身体重要还是工作重要?老婆 重要还是老妈重要?……

这些问题不解决问题我们都知道,但如果我们有三个假期的时间,这三个假期用来补语文 还是补数学呢?补语文还是补数学就有个“选择”的问题在里面了。这就是我们架构设计常 常面对的问题了:选性能还是选功能?

这个选择每天都在变,架构设计怎么写?

架构要写的是:(我们认为)这个市场是如何认识性能的,是如何认识相关的功能的,我 们现在处于什么实现水平,我们曾经做过的选择是什么,我们当下的选择是什么。

这样带来的好处是,让主要矛盾可以被用更快的方式突出来,让每个子角度受到一定的压 力,把资源投资最重要的逻辑上。比如你做一个内存分配算法,很多人上来就开始考虑“线 程安全”,但“线程安全”不是核心诉求,“分配内存”才是,这样,“线程安全”当然好,但在 没有看到足够收益前,这部分逻辑你就要给我退回去,让路出来给我处理其他逻辑,比如 我先考虑使用这个内存分配算法的那个系统的执行模型,直到那个执行模型不得不需要线 程安全的时候,你再给我加上这些要求,才是合理的。

这就是架构设计,或者说高层设计,相对代码的不同。越高层的设计,写得越是恍惚。这 也就是王小波在《万寿寺》里面说的,“一切都在无可挽回地走向庸俗”,当我们从恍惚最 终走向具体,所有的浪漫,终究会走向庸俗。但我们需要的是,在我们终究走向庸俗的时 候,我们是真得没有问题要解决了,而不是我们半途而废了。

todo:文中选的例子我都不太满意,但正好有个灵感,就先把逻辑建出来,看看以后能否 找到更好的例子来替换吧。