仓库源文

.. Kenneth Lee 版权所有 2016-2020

:Authors: Kenneth Lee :Version: 1.0

反者道之动——欣赏架构设计的基本逻辑


程序员圈子的文化是种挺“务实”的文化,他们为更高的性能,更复杂的功能叫好,给夸夸 其谈扇耳光,这些都看起来很美。但没有什么东西能在各种场合都保持美的,这种文化, 其实也给架构师带来挑战。因为架构师基本上就是“夸夸其谈”的人物,至少看起来如此。

我在实际工作中提出的设计要求,经常会受到一些(我认为是低水平的)一线程序员的挑 战,他们的基本逻辑是:我这个不是一样能跑?性能也不比你那个低(甚至更高),完成 时间也比你更快,凭什么要听你的?

这在外行听来还挺有道理的,在没有经验的人群眼中(比如在投资者或者市场向的产品经 理眼中),无法比较这两者的区别。这种场合,我通常不解释,在没有名称空间作为支持 ,解释我认为的真理?我没有那么白痴。这种场景需要另一个名称空间:我是专家,这个 产品我负责任,产品失败了我自然会负责,所以设计方向不关你的事,好好干好你自己那 一part。这句话我现在这样表达出来,部分读者听着也许不舒服,但这不过是个怎么“加花 ”的问题,有很多方法可以soften这个语气(那个要看面对什么人,这里不再细述),但肯 定比用技术理论来解释实际得多。

有些东西,有经验的工程师能看得出来。我接手过一个Android机顶盒产品的架构管理,刚 上手的时候,看到他们在做一个需求:为设备增加红外遥控的关机功能。在我看来,这个 功能不用想,怎么都应该在input子系统中增加本平台的编码转换,然后调通中间evhub的 通道,报给上面的KeyManager,然后从那个地方调PowerManager流程来完成吧(原理性的 ,我没有去详细跟过具体流程,对付着看吧)。所以我猜这个功能怎么都得做上个3,5天吧 。结果这位哥们第二天就交货了,我靠,天才啊。然后我看了看他的Patch,实现得那叫一 个简洁:直接在中断处理中增加一个判断,过滤出是我们平台的那个Key,然后写硬件寄存 器,从PMU把电给断了——搞定。WTF!

我在站立例会上把他骂个狗血淋头,他还一脸懵比,觉得我是下车伊始,拿他找存在感。

这种问题,对很多稍有经验的Android平台维护者来说,显然能明显感觉到设计上有问题( 比如直接断电导致文件系统的损坏,还有未来增加新的按键出来带来问题等)。这个不是 我这里要讨论的问题。我要讨论的问题是:这个问题的核心问题是什么?为什么这种“也解 决问题”的方案,在架构上会被认为是一种失败的举动,如果架构不解决眼前的问题,那么 ,在“眼前此刻”,架构的设计者到底在关注系统的什么指标?为什么我们值得在这个指标 上花费额外的投资?

如果软件不需要修改,不需要发展,“能跑就好”的逻辑是没有任何问题的。一切架构设计 逻辑,都是为了未来,而不是为了现在。用现在能带来的好处来为架构设计行为辩护,都 是无力的,好的架构师不能用这个逻辑来为自己的设计做辩护。

为未来又为的是什么呢?我们发展软件,是为了能力的增强,能力的增强,就带来了逻辑 的增加,比如你以前用Windows,把一个文件夹拷贝到另一个文件夹,遇上同名的文件夹, 开始Windows只会告诉你:文件夹已经存在,拷贝失败。后来你可以选择放弃还是覆盖。现 在你就可以选择放弃,覆盖,合并等等。这些是能力的增加,但从软件来说,这本质是逻 辑判断的增加,是逻辑的逐步细化。这是软件的发展。

软件发展,或者说逻辑量的增加,带来两个变化:一个是软件能力的增加,另一个是软件 复杂度的增加, 从我们想得到结果上来看,前者是个好事,后者是个坏事。后者是熵增的 过程,熵增带来软件的死亡,熵增超过人脑可以控制的程度,这个就不再是有规律的系统 ,而是个混沌的系统,混沌系统完全不可预期,不可控制,这个系统对想控制的人来说, 所代表的就是死亡。

我们要延缓软件的死亡,就要降低熵增的进度,保留中心机体的活性。从这个角度上来说 ,开发的工作目标是“增加”,而架构设计的工作目标是“减少”。这和人的成长很像,你把 睡觉的时间都用于工作,你的收入一定会增加的,但你很快就死了。架构设计的目标就是 在完成软件能力的提升的同时,减少熵增。我们愿意花工作以外的时间和精力去健身,去 养生,去吃补品,目的都是降低自身的熵增过程。

但逻辑量增加本身就是一种熵增,禁止熵增的唯一方法是不增加功能,这个不能作为架构 控制的目标。所以我们只好退而求其次,把我们的目标设定为:在完成功能的同时,对系 统增加最少的依赖。我们欣赏架构的好坏,一个基础的判断逻辑就是这个:如果我们承认 我们有共同的设计目标,优美的设计创造更少的依赖。

从这个角度再看前面的问题,我们就发现了,如果我们通过输入映射表的方式来实施遥控 器关机的功能,虽然工作量增大了,但我们给系统增加的新的约束非常少,这些约束都不 在主流程中,中断仍然只是上报事件,input子系统仍然只是做事件编码,event_hub仍然 只是做汇聚,我们不改变它们的角色。但直接在中断中访问PMU?那其他关机前要做的操作 是不是要跟你的中断处理流程做同步?那些流程要不要关中断?关中断后怎么处理多核时 的互斥,加上互斥以后,性能下降和死锁问题如何解决?……你看,这么一个简单的修改, 踩到多少人(模块)的雷区?

所以好的设计,我们追求以天下之至柔驰骋天下之至坚,像风一样穿过大地(系统),不 带起一丝尘土(不破坏任何已有的规矩,至少不破坏已经非常坚硬的规矩)。

从这个角度来说,我们欣赏一个构架好不好,或者我们自己判断我们的一个设计是否合理 ,基本逻辑就是:它为了解决一个问题,到底增加了多少逻辑,是否有多余的逻辑,布置 在其中而不自知。

为了能欣赏这个问题,我们在讨论任何这类型的问题时,首先就要定义我们要解决的问题 是什么,特别是确定它的范围是什么,这样我们才能评判一个设计是否合理。

这里有人问了一个问题: https://www.zhihu.com/question/48212046/answer/109694471?from=profile_answer_card

平时我们看到不少人不理解为什么这么个简单的设计,要描述得如此复杂,我把这种理解 称为“缺乏构架思维”。如果我们用“用最少的依赖解决必须解决的问题”这个角度来思考这 个问题,我们就会发现这个定义是非常精巧的:

首先,它要解决的问题是:如何存储指针。int * a,这是必须的,因为我们需要一个针对 变量的索引。实例不能取代这种功能,索引可以起到“实例的值更改了,使用索引去访问这 个值的时候,这个值也更改了”这个功能。但索引也是一种变量,我们有可能需要把这个变 量存到其他结构体中,里面如果有一个int,我是否可以临时用来放一个指针呢?从当前实 现上看可以,但未来是否还是可以,很难说。我们以前还以为int都是16位的,以为a=3就 是内存中这个值变成3,以为0xfffff就是-1呢?实际上,现在这些可能都不成立。

所以,我们设定规矩的时候,就要仅仅设定“解决问题”仅需的规矩。弱水三千,我只取一 瓢。所以这个规矩就是,你可以把指针写入一个超过大小的变量,我也保证你取回来的时 候,这个变量还是那个变量(这对很多变量类型来说可不一定可以保证的),但其他的任 何东西,我都不承诺。

反者道之动,当我们为长远来考虑的时候,我们整个决策逻辑和结论,就是我们眼前习惯 的逻辑反过来了。要欣赏构架之美,首先要学会欣赏简单之中见复杂之美。