.. Kenneth Lee 版权所有 2016-2020
:Authors: Kenneth Lee :Version: 1.0
解耦设计
一般设计设计现在,构架设计设计未来。设计未来一个很重要的考量要素就是“解耦”设计 。
直观理解“解耦”,就是我可以替换某个模块,对原来系统的功能不造成影响。比如Windows 95, 98, 2000, 7, 8, 10都可以直接运行在兼容PC上。反过来,奔腾,奔腾II,Core做出 来的不同硬件,都可以跑Windows。
在构架设计中,解耦是一种成本很高的设计,因为比如说你本来点亮一个LED灯,是可以这 样写的:::
*(volatile u32 *)(0x1234567) = 32;
但在为了解耦,你需要这样写:::
#ifdef PLATFORM1
*(volatile u32 *)(0x1234560) = 32;
#elif defined(PLATFORM2)
*(volatile u32 *)(0x89abcde0) = 128;
#elif defined(PLATFORM3)
*(volatile u32 *)(0x89abcde0) = 256;
mb()
*(volatile u32 *)(0x1234560) = 32;
usleep(10);
*(volatile u32 *)(0x1234560) = 64;
#endif
如果我们不约束接口,解耦设计就会变成一个僵梦。我都不说增加的代码量了,就这种一 堆#ifdef,就能让你看得头都大了(qemu的代码就是其中一个坏例子,和Linux Kernel的 代码对比一下就能比出来,但因为qemu的复杂度比Linux Kernel还是小多了,维护者们都 不在乎而已)
很多工程师解决这个问题的方法是设计“灵活接口”,这种灵活接口包括(而不限于):
i. 使用“消息接口”取代函数或者寄存器接口
ii. 使用“命令”取代寄存器接口
iii. 使用HAL层“封装”差异
iv. 用配置文件改变模块的不同行为
我这个文档要表达的观点是,就用这种方法实现“解耦”,基本上是缘木求鱼,水中捞月。
我们前面已经说过了,“解耦”的目的是可以替换接口上的一个模块。那么接口通讯变成消 息了,改变了两个模块之间的依赖关系吗?显然没有——那有什么用?你把消息通讯上的一 个模块换了个版本,或者把配置文件调整了一下,编译器确实没有报错了,但模块的行为 如果发生了改变,这个程序还能正常运行吗?(如果能正常运行,你用函数接口又有何妨 ?)
这个理由对于硬件的寄存器定义是一样的,用“命令队列”取代原来直接的寄存器访问,看 起来写寄存器是不会报错了,但命令字进入硬件,你还能正确处理吗?如果你能正确处理 ,是命令队列还是寄存器,又有什么所谓呢?
所以,想简单引入一个这样的方法来说明自己进行了“解耦”设计是很不靠谱的。
HAL也是,比如前面那个点灯的操作,你抽象这样一个接口:::
void light_switch(bool on_or_off);
看起来你抽象了差异。但假如你后面选择的灯是有多级亮度的呢?你的亮度从什么地方传 递过去呢?
很多人都看不懂Android的HAL设计,Android的HAL称为“硬件抽象层”,但它的设计不是来 自对硬件的抽象的,它是来自对用户的需求分析的。我在有短信的时候要点亮呼吸灯,我 抽象这样一个接口:::
void light_on_message();
我不管你选了什么灯,也不管你是几级亮度,我只要你能说明现在来消息了,你别找我要 亮度这个数据,我不关心。这样这个接口就比较有保证了,这才是HAL的本意。
所以,HAL的设计基础是需求分析,不是硬件设计抽象,直接进行这种抽象而跳过市场预判 和需求分析,是一种鸵鸟政策,不肯面对自己要面对的最难的问题。
除此以外,在很多时候,仅仅考虑数据传递的充要性是不够的,我们还要面对性能引起的 细节问题。比如我做一个加速器引擎。用户态给定一组数据,按某种格式A排列,用系统调 用进入内核,驱动的格式是B,这样就需要做一个转换,然后写入硬件,结果硬件要求的格 式是C,这又发生一次转换。这个过程中,接口倒真的不需要修改。但这样一来,还加个鬼 速啊。
所以,用户程序,驱动程序,加速器硬件的设计者,是没有任何机会可以各自为政的,你 们必须坐下来,先完成你们对这个特性的需求分析,考虑所有未来的变数,然后定义一个 可以从上到下贯通的接口,才能独立聚焦各自的模块做模块设计。
这说起来是个简单的道理,但一旦交付压力上来,设计师们就开始装傻了。但我告诉你, 项目出问题的时候,倒霉的肯定少不了你,别以为你那些小聪明能帮助得了你什么。