3.255. 一个逻辑链断裂引起架构设计方向错误的实例

最近评审了一个构架设计文档,看到一种很常见的错误,我在这里表述一下,供其他设计者参考。为了保密和突出描述重点,我描述的时候会对具体情况进行加工,所以这里描述的不是针对某个真实的情形的重现,而是表述一种常见错误的关键Pattern。

这个设计用于一个系列设备的驱动开发团队。驱动团队,有经验的工程师应该都知道,就是你做了一个硬件,这个硬件里面可能有USB,有存储,有GPIO,有网口,说不定还有各种加解密设备……诸如此类的,这些硬件不是标准的接口,要让操作系统去用它,需要一个“最后一公里”的桥接过程,完成这个桥接的,就是驱动。

如果你用了某个操作系统,这个操作系统就会对这些驱动有约束,这样天然会形成一层架构,所以,很多时候,驱动团队是不怎么需要做架构设计的,特别是很多设备的嵌入式驱动其实都不大,几千行就够了,内部的结构也不复杂,所以内部的逻辑也没有太高的层次,也不需要到架构这一级的设计。

但这个团队发展到一定的规模了,也开始有不少的历史包袱了。他们有这样一个需求:操作系统(连上驱动)必须可以前后兼容。简单说,旧的操作系统版本(二进制,下同)可以运行于新的硬件,新的操作系统版本可以运行于旧的硬件。

这种设计跨越多个版本,多代硬件,还需要驱动之间有一些公共的框架可以配合。这就不适合放到每个具体的驱动的设计里面去做了。这种情况,就需要做一个架构设计了。

团队的架构组开始用4+1视图的方法来做这个架构。首先,他们给出了设计的目标,就是前面这个需求,要实现硬件和软件的前后兼容。

然后他们开始画Use Case图。好了,问题就出现在这里了。我们说过(参考:Use Case图有什么用?),Use Case图的用途是划定我们做的功能的范围,决定它呈现为一个什么产品,以及这个产品在整个使用场景中的位置是什么。

所以,就这个实例来说,架构师很容易就开始画“驱动在系统中处于什么位置”。这个描述本身“没有错”,但逻辑链断了。你做这个设计的目的不是解决兼容性问题的吗?你突然描述驱动有什么用,这个逻辑在逻辑链中的地位是什么呢?你在证明什么呢?

这是一个典型的“每个单独的表达没有错,但在逻辑链证明上是错误的”的设计错误。

我们对比一下把问题域定位在“做驱动这个功能”和“解决前后兼容问题”这两个不同目标的时候,Use Case有什么不同:

../_images/%E9%A9%B1%E5%8A%A8%E5%85%BC%E5%AE%B9%E7%9A%84UseCase.svg

你就会发现,架构视图其实有非常高的自由度,看的是你抓逻辑中的哪个点。你抓功能这个点画出来的图,和你抓兼容这个点,画出来的图,就会完全不同。离开了逻辑链,我们根本就没法评估一个架构是否正确。

一旦这个点错了,后面你画什么视图都是错的,而且这种错误会一路扩展下去。比如概念视图。如果你针对功能来进一步建模,你就只能讨论网口如何使能这类逻辑,你建立的概念就会是:设备总线,设备发现,网络框架,协议栈……这些东西。而这些概念,我们一开始就讨论过了,这没有什么高层设计逻辑,因为它是被操作系统约束的,不是被你的设计约束的。

如果你基于的是前后兼容的设计来考虑。你建立的概念空间就会是:新硬件的范围,旧硬件的范围,对于某驱动版本X,如何确定它所支持的新硬件和旧硬件,如何定义X的定义域,对X需要执行什么约束,才能保证它支持它的新硬件和旧硬件集合,支持到什么程度。

你看,这完全讨论的就不是同一个问题。而且,显而易见,这会是一个非常脏的问题,因为它不可能是个平直的问题。你现在在外面已经有多少软件版本了?这些版本都要支持你的新硬件,你怎么保证?如果你没有定义过标准,你要新硬件一个bit都不能改吗?就算硬件一个bit都不能改,一样有不兼容的问题,那些因为Cacheline长度变了,就让性能大跌的问题还少吗?因为Order变了,导致完全符合PCIe标准的卡插不到特定的PCIe接口上的事情还少吗?你要从这种泥潭中抽出来,就得做揪心的选择,比如我首先支持一组版本,这组版本优先测试和保证。另外我可以留下一些弱控制,比如要求所有硬件都要给我留一个revision编号,保证我要做errata的时候还有一个可以和统一管理版本的地方。

我们说构架设计难,不就难在这种地方?你既不能不顾现实,不去理解细节,你也不能陷在细节中,不去抽象简单的接口,保证系统持续可控。(中间完整索引《中庸》的段落会被gitee算法屏蔽,删除这一段)所以白刃可蹈,而中庸不可能也。逻辑的难度在于准确,而不是选左还是选右。