仓库源文

.. Kenneth Lee 版权所有 2018-2022

:Authors: Kenneth Lee :Version: 2.0 :Date: 2022-09-07 :Status: Released

理解关联


在工作中,常常看到一些工程师对关联的本质缺乏认识,浪费了不少讨论时间,本文细化 一下这里的逻辑。

当我们说两个对象有“关联”:

    .. figure:: _static/关联.jpg

直接感觉,是A和B总是以某种方式发生关系,比如一方或者两方拥有对方的指针(甚至是 内存本身),例如:::

    struct A {
      struct B *b;
        ...
    }

或者:::

    struct A {
      struct B {
        struct A *a;
      }
    }

这种关系可以变得更加复杂,比如A中有方法可以调用B:::

    class A {
            void dealWithB(struct B *b);
            ...
    }

或者另一个对象把两者关联起来:::

    class C {
            Map<class A, class B> ab_map;
    }

甚至可以从表面上看不出来的,下面是一个从表面上看不出的关联:::

    class A {
            void storeStudentData(FILE *file);
    }

    class B {
            void takeStrudentRecords(FILE *file)
    }

A把特定的数据存到文件中,B从这个文件中拿这些数据,A和B就有了关联。

严格来说,你可以说一个系统中任何两个模块有关联。所以,我们不是为了说明两个模块 有“关联”,所以就要在架构说明的时候就说两个模块有“关联”,我们是因为这种关联对我 们的的下一层设计有影响,所以我们才需要在架构定义的时候说明它们有“关联”。

所以,关联其实关心的是两个模块的“同步”关系。

所谓同步关系,可以表现为多种形式,比如:

A的逻辑发生了修改,B也必须同步修改,整个系统才能是正常的。前面这个文件的关联就 是一个例子,如果你修改了A保存文件的方式,虽然整个程序编译一点问题没有,但如果你 不修改B,这个程序就不可能正常。

所以,很多人觉得,把函数接口变成消息接口,文件接口,这个系统就“解耦”了,这是在 自我安慰。我们从架构上从来不这样看问题。关联是因为两者在业务上有关系,代码的逻 辑链在逻辑上对另一方的设计方法有“依赖”,不消除这种逻辑上的“依赖”,就不可能“解 耦”,这你怎么玩各种定义都是玩不出花来的。而消除耦合的唯一方式,是消除多余的“依 赖”,比如我只需要你是个指针,不需要你是个unsigned long int,你就不要说这个变量 是unsigned long int;你打印指针就用"%p",你不要用"%xl";你要给我传递“学生数据”, 你就给我"interface student",不要给我“class student_implement_on_file”,这样 就能消除耦合,但每个消除耦合的动作都是额外的工作量,所谓你必须在聚合了很多关联 的地方建立有限的解耦设计,这才能在工作量和解耦方面取得平衡,架构设计从来不是可 以被简单描述的工作,否则,它就变成一种“编码”了。

再说远一点,昨天评审了一个设计,谈到一个软硬件之间的FIFO接口,设计者兴高采烈地 给我介绍了一个精妙的设计,叫做xxx_id,我听了半天,原来逻辑是这样的:软件要写一 个请求到硬件上,不能直接向Ring Buffer(循环队列)的尾巴上直接写这个请求的内容, 而要在里面找一个已经释放的BD(Buffer Descriptor),再把这个BD的下标(称为xxx_id ),写到另一个叫Queue的数据结构中,硬件会按FIFO的形式从Queue里面读xxx_id,然后得 到这个BD……

我理解完这个意思就发现了:这不就是把FIFO实现到Queue里面吗?从架构的抽象层次来说, 这就是一个FIFO,你用Queue去写这个FIFO,写FIFO的时候整个BD写进去,还是写一个头进 去再用指针指向块身,还是用一个tag去另一个表里面匹配块身,这个高层抽象都是FIFO。 如果你高层逻辑不改变,你无论用什么方法封装你这个高层逻辑,关键本身是不会改变的。

其实,关联关系在文字上特别容易暴露。如果你总是用 :doc:逻辑闭包 <逻辑闭包V2> 来进行设计。那么两个逻辑闭包提到对方的逻辑,这两个逻辑闭包就产生关联。如果这两个 逻辑闭包属于两个对象,那么这两个对象就发生关联。我们要降低耦合,多用逻辑闭包来 解决问题就可以了。

比如说,A模块提供a_malloc和a_free两个接口用于分配资源。那么它和B模块就没有关联。 但如果你说,a_malloc需要B模块进入休眠状态才能调用。说明A要实现逻辑自恰,不得不 提到B,你就必须创建关联。你要消除这个关联,想办法让自己不用提到这个B模块进入休 眠这件事,你就成功了。否则无论你怎么优化接口,结果都是一样的。

我们经常描述上的便利,而创建了额外的多余关联。比如说,你定义一个指令架构,你说, 我执行一段代码,如果发生了错误,就会进入异常处理流程,在异常处理流程中必须清楚 错误标记,这样后续的代码不会再次触发异常(不要和我掰你现在的处理器不是这样的, 我这是举例,也许我的处理器就是这样的呢?)。好了,我这个定义中,其实根本没有提 我的程序是什么特权级的,也没有说哪个特权级处理这个异常。没有经验的架构设计人员 就会不由自主地具像化这个问题,说“如果发生了错误,代码会切换到内核态,异常处理向 量就会如此如此,这般这般”。好了,这个描述就直接让这段逻辑和“内核态”,“异常处理 向量”建立关联了,如果你想用于“Hypervisor态”,想换成“异常处理协处理器”,这段定义 就需要改了。

把逻辑闭包用文字表述出来,会让我们很容易看清楚关联是什么,也有助于我们用手术刀 级别的精准,去减少关联“。