仓库源文

基于逻辑链建立约束


上一个文档::doc:管理上的判断和技术上的判断 ,引入了一个我自己很喜欢的概念: 抽象的本质就是减少属性。

    | 注:这里说的属性,有两种。一种叫原始属性,也就是说我们直接感受
    | 到的属性,比如我们描述对象的颜色,大小等等。另一种属性叫推理属性,
    | 比如红色必然是其物体反射光谱波长760-622纳米的实体,这是基于其他
    | 感观认识和逻辑推理得出来的。这两者究其根本,其实无所谓,因为我们
    | 感受到的属性和物体的“本质”是什么属性,我们并没有能力区分,我们已
    | 经被自己的感官能力左右了。强调这两者,只是为了让我们意识到,
    | 属性是可以被“等价的”。当我们说某个对象的x=3,y=4,这和说它
    | 的z=7,y=4这个描述是等价的。即使是相同的信息,都可以呈现为完全不
    | 同的形态。

换句话说,当一个对象包含很多的属性,记为:

    .. math:: Obj(a|a \in A)

那么, .. math:: Obj(b|b \in B, B \subseteq A)

就是前者的一个抽象。

这个定义写得很“数学”,但它并不严谨,有两个问题只能让读者自己注意:首先由于前面 说的属性等价关系的问题,我们没有能力把集合A化简为一个“原始属性集合”。原因前面也 提过了,我们被自己的感官左右,无法判断什么属性是原始的,甚至无法知道是否存在原 始的这么个东西,这个需要额外的信息,而这个信息我们没有。

其次,由于前面这个原因,不存在一个可以表达某个对象所有属性的集合,甚至我们不能 假设这个集合存在。但我们后面的推演,为了说明抽象这个Pattern,我们会使用一个我们 “关注”的全体属性的集合,这个地方是有破绽的,但它有助于理解问题,这一点请读者注 意。

软件或者系统架构,我们最终的目的是定义一个概念空间,里面描述了所有的对满足某个 需要(目标)的一组约束。这样,使用这组约束,我们就可以重复得到相同的目标。比如 一个Hello World的程序,它约束了可以使用的操作系统,动态库,命令接口,当你重复运 行这个程序的时候,它每次都能输出“Hello World!”。甚至,你每次重复编译这个源代码 ,它在不同的平台上,也都能输出“Hello World!”。

上面这个描述,我们已经给出了一个抽象的例子了:源代码形态的“Hello World”程序,是 二进制形态的“Hello World”程序的一个抽象。前者的属性是后者的一个子集。

建立前面这个概念,有助于我们正确理解架构设计的很多基本概念。架构设计是基于抽象 的概念空间设计(或者说针对设计目标的一组逻辑链)。比如我们设计一个Hello World程 序(为了让问题复杂一点,让我假定我们什么基础设施都没有,就是裸跑),我们最初建 立的逻辑链可能是这样的:::

reset_handle:
  io_init();
    mem_init()
      buf = mem_alloc(MAX_ENCODE_BUF_SIZE);
        encode_string_to_io_format("Hello World!", buf);
          io_put(buf);
            mem_free(buf);
              mem_finilize();
                io_finilize();
                  halt();

io_init()是什么呢?需要干什么呢?我们还没有想,encode string用什么格式呢?我们 也没有想,但如果我们纠结于这个格式,io的设计者就会向主流程的设计师要格式,而主 流程的设计师又会要io的设计师要格式。这件事情就没法做了。因为在这个模型中,主流 程的设计师掌握了部分的属性,而io的设计师也只掌握了部分属性。就算这两者是同一个 人,他想问题的时候也无法掌控无穷无尽的属性集合。

解决这个问题的方法是:先由其中一方(通常是最靠近目标逻辑的一方),给出第一层逻 辑抽象,针对目标建立逻辑链,形成一个最小约束,然后等被约束的其他对象反馈细节上 的约束,从而逐步细化出最后的结果。

还是上面这个例子,高层架构掌握的情况是:

    (图片丢失)

系统需要从芯片的复位向量上开始启动运行,运行完成后需要停机io设备需要初始化系统 的内存需要统一管理io设备可以按一定格式输出内容

只要这些条件可以成立,这个逻辑就可以通了。至于io_init()是不是需要给定一个超时参 数,它“不知道”。你看,这就是所谓的“知不知上”了。当我们进行高层设计的时候,明确 确定“不知”的范围,其实是给细节设计留下设计的自由度。这样,当我们具体设计mem,io 这些子模块的时候,遇到任何障碍,都有足够的逻辑空间可以腾挪。

但如果你想得多,非要把io_init设计成这样:::

    io_init(io_addr_base, serial_port_bandwith, interrupt_id, latency);

你就已经进入io这个子模块的设计空间了,你又没有深入调查io这个模块的所有情形,也 不是要取代io设计团队,推演的时候也没有自洽,这个设计就是失败的。你给定了串口的 波特率,就已经要求io必须是串口了,它难道不能是显示器的?这时,你就需要设计“io必 须用串口”这个独立逻辑,证明所有选择中串口是唯一的选择。

而且,即使你真的这样做了,你取代了所有的细节设计,一次成型,你这个架构设计都是 失败的。因为你放弃了把不同的逻辑分解到不同的隔间,让每个独立的部分可以聚焦到自 己的独立逻辑上,这会导致你的系统失去单独替换某部分逻辑的能力,以为你对系统任何 一个部分进行修改,你都要确定一次,所有其他的逻辑是否被改变了。

这就是本文要描述的问题:在每个抽象上,用你自己知道的信息去给其他抽象制造约束, 而不是插入到别人的约束设计中。架构师可以下来帮你做一个模块的设计,但在架构设计 的抽象这一层,他不应该知道的东西,他需要保持“不知道”。

在上面这个例子中,高层逻辑的设计者仅知道机器状态管理,io和内存可实现,用户需要 在IO上输出“Hello World”这几个基本的信息,其他细节他一概不知。如果强行“知道”,抽 象就变成了具象,也就失去了抽象的作用了。

这也解释了为什么设计不是越细越好的,同时也解释了为什么即使不细,他的逻辑链仍是 严密的。因为基于抽象的设计,逻辑是建立在一个对象属性的子集上的,那么,所有包含 这个子集的对象,都在设计范围内。但你在逻辑中增加了额外的限制,而这个限制又不是 你实现目标的逻辑链必须的,你的利益就不会得到最优化了。

所谓不敢为主而为客。什么是为客?为客就是“客随主便”,是被动的一方,是迫不得已的 一方。我需要输出"Hello World!”,我不关心io子系统是不是串口,你变成显示器,变成 LED数字屏,甚至变成地外太空发射信号都行,我这个模块的逻辑都是成立的。这叫有自由 度。没有目的地创建约束,好像在做架构设计,其实是制造垃圾而已。垃圾的结果不是没 有用,你家里有垃圾,不是多了很多没有用的东西,而是制造疾病,改变家人的生活方式 ,甚至导致老鼠蟑螂白蚁横行,最后房子就塌了。软件构架中制造多余的逻辑,不是产生 没有用的逻辑,而是其他设计都不得不绕着它走,最终整个软件都走不下去。

架构留下余地,也是给自己留下空间,比如前面这个高层设计,我留了一个明显的(但不 一定存在)的破绽:io子系统通常也是要用内存的。所以,在io初始化后再内存初始化, 这可能不行。所以,如果io推演的时候发现了障碍,是需要反馈回来给高层设计的,高层 设计再综合这个逻辑来整理全系统的约束。但因为我们是基于目标来整理我们的第一层逻 辑的,即使这一点判断错了,我们调整起来仍是比较简单的,因为基于这个目标来建逻辑 链,你能做的选择本来就有限,最终该做的事情还是要做的。

换句话,架构的逻辑本身就不是一成不变的,而是根据新发现的问题不断进行调整的。但 大部分时候,我们不轻易提前引入约束。比如我们这里说io子系统可能也要用内存,但内 存子系统可能也要用io呢?没有这个信息前,提前做这种设计,就是过度设计,很容易制 造多余的约束的。

补充1:这个文档其实也解释了那个《:doc:“解决方案” 》中提到的问题。架构设计实际 上是完整设计的其中一步,是刻意留下设计余地,以便下一层设计可以在这个基础上进行“ 创造性”的劳动。但如果你对它的预期是“可以照着执行”,那其实你就是把自己看做是死物 了,并不认为是一个创造者,而是一台机器。而软件业,机器是计算机,不是人。