软件是一种固化的思维→思维的固化体现为概念和逻辑的固化→为保证简单性,逻辑要尽可能的少→概念要尽可能正交。
1.正交的基本定义
软件开发中最为核心的任务之一是分类,分类即是一种打造概念边界的过程。
打造概念边界的难点在于,概念本非有形之物,且概念之间又多有重叠、相关之处。这也就注定概念边界的澄清是一个渐进的过程,很多时候必须要进行迭代,进行反复,才能贴近最优的答案。
概念本身的边界究竟在哪里在大多时候并不只有唯一答案,存在模糊性。比如,当我们描述一本书的时候,那么这本书是否被借出了这种信息,是既可以作为书的基本信息的一部分,也可以作为借阅人的基本信息的一部分。
大多时候一旦切换视角,相同的概念又可以有多种划分方法。好比说,我们可以很容易界定什么是人,什么是猿,但当我们试图把人猿归类的时候就得依据我们的视角进行抉择。因为它似乎是人,似乎是猿,也可能是人猿。打造概念边界时正是类似人猿这类概念让我们犯难。
庄子对上述现象进行过非常形象的描述,他说:自其异者视之,肝胆楚越也;自其同者视之,万物皆一也。
对上述问题,大多时候只有选择而没有答案,只不过选择背后所隐含的成本和收益往往不同。
打造概念边界时原则可以有许多,但主要手段只有一个,即抽象。从本质上讲,抽象是一种认清事物本质,并进行归类的过程。抽象是设计工作的起点,而抽象的结果可以是一个具体的概念,也可以是一段逻辑。
正交性则是抽象时最为关键的一条原则,不正交的概念往往是含混的。为了理解正交性,我们先来看一下如图7-2所示的几何含义。

图7-2 正交的几何含义
当两根直线互相垂直的时候,我们认为这两根直线是正交的,否则的话这两根直线就是不正交的。这似乎和软件没什么关联。
如果我们假设相交的不是两根直线,而是两根圆柱的话,那么我们就可以看出来正交和非正交的差别。在正交的情况下,两根圆柱的最大接触面积始终会等于圆柱截面的面积,但在非正交的情况下,接触面积则要大于圆柱截面的面积,并且倾斜度越大,接触面积越大。如果这两根圆柱是木材的话,那么接触面积越大,施工量越大,木材的可替换性也就越差。
概念或逻辑关系正交与否,其影响与上述类似。假设说我们定义了两个类,类XMLReader负责具体读取XML文件中的节点,类XMLDataHandler负责加工从XML文件中读取出来的数据。这个时候如果在XMLDataHandler中出现了根据XPath读取XML内容的方法,那么这两个类无疑的会变成非正交的,如图7-3所示。

图7-3 非正交现象
因为读取这一功能既存在于XMLReader,也存在于XMLDataHandler。在这种情况下,这两处地方都和XML的结构产生耦合,如果XML的结构发生变更,那么这两个地方都需要变更。同时也会导致程序中存在相似度比较高的代码,增加不必要的复杂度。
上述这类不正交的情况,有时候会被称为耦合,有时候会被称为不充分的抽象,但不管怎样,其根本问题在于概念或逻辑的非正交性。
不正交的情形有很多,但总结起来,这些情形大致可以分为两个类别,这两个类别与软件概念间的基本关系有关。
如果要把软件中的概念间的基本关系做个分类的话,那么大致可以分为两类:一为明确一种层次关系,不同的部分做的事情事实上是重叠的,但具体的程度不同,我们把这种关系称为横向分割;二为明确彼此关系,即你是什么,我是什么,我们把这种关系称为纵向分割。这种分割具有递归特质,概念或逻辑内部又可以进行新的一轮分割。
●横向分割产生“层”的概念。
比较经典的例子有OSI的网络模型、Windows的GDI设计等。这里以Windows的GDI设计来做一些说明。
Windows一直强调一个所见即所得的概念("WYSIWYG"),也就是说屏幕上用户看到的内容应该和打印机上打出来的内容保持一致。如果应用程序(如Word)与显示器的特性,乃至打印机的特性直接相关,那么几乎没可能达成这一目标。为解决这一问题,Windows中采用的办法是在具体设备和应用之间架起一个新的层次,这个新的层次即GDI(Graphics Device Interface),其替代技术叫WPF(Windows Presentation Foundation)。
最终Windows中显示这部分的基本结构如图7-4所示。

图7-4 Windows中的层次
注:这里只画出示意图,可能会给人一种错觉,感觉层次的切分是种容易的事情。如果有谁真的这么想,那就错得利害。写过显示器驱动和打印机驱动的人想必能了解定义层次间的接口是多么的困难。
这种情况下,GDI层和Driver层做的事情本质相同:即向指定页面描述指定内容。但具体描述方法不同,GDI较少关注设备特性(或者说只关注设备通用特性),而驱动程序则要关注设备的特有属性。很多设计手法,其本质都是在软件的结构中加入更多的层次。像我们常说的Proxy,Facade模式等。
●纵向分割则产生模块或对象,经典的例子是MVC等模式。Model、View和Controller其实是不同的概念,但它们彼此间有联系,所以这3个相对独立的概念要经过某种关系连接在一起。
横向分割的时候,不正交体现为抽象层次上的不一致性,比如在Driver层面还做许多GDI层面应该做的事情;纵向分割的时候,不正交体现为重叠区域的存在,比如,在View中直接对数据进行处理,如图7-5所示。
从上面的例子可以看出,正交性强调的是只让概念或逻辑在必须关联的点上产生关联。这里事实上包含了两个基本命题:一是不重叠;二是接触面要尽可能的小。不重叠说的是两个概念间没有重复的部分,这很好理解。接触面最小则有点抽象,我们来结合一个例子进行说明。

图7-5 正交与不正交的示意图
2.正交程度的优化
在《敏捷软件开发:原则,模式与实践》的第12章中,解释接口隔离原则(ISP)时提到了这样一个例子:
在安全系统中,有一些门,这些门可以被加载和解锁,并且知道自己是开着还是关着。

接下来,一种更高级的门出现了,这种门如果开着的时间过长,就会发警报,这种门被称为TimedDoor。因为要定时触发某些事件,所以TimedDoor要用到定时器,而定时器的基本创建机制是:

任何TimerClient都可以向Timer注册自己,而Timer则会按照指定的时间间隔来调用TimerClient的TimeOut()。到现在为止,Timer、TimeClient、Door在概念上是正交的,没什么问题。
接下来,为了使TimedDoor具有定时发警报的功能,这3个概念要产生交互了。RobertC.Martin在书中给出了3种可用的交互方式,我们这里简单引用其中的两个,并附加一个自己的解法,来演示正交程度的逐渐提高。
●第一种方法Door从TimeClient继承,UML图如图7-6所示。(https://www.xing528.com)

图7-6 定时门的UML图
这种方法的正交程度最不好,接触面过大。像RobertC·Martin在书里说的:很多Door根本和定时不定时没有关系,但一旦让Door从TimerClient继承,那就不管什么门都有了定时的特征,这种关联毫无道理。
●第二种方法是让TimedDoor分别继承TimerClient和Door(多重继承),UML图如图7-7所示。

图7-7 定时门的UML图
RobertC·Martin认为自己会优选这个方式。
从正交的角度看,在这种方式下,TimedDoor只和与自己有关的部分产生关联,正交性已经非常好了。
●如果仔细想想,就会发现第二种方法其实还是有问题。
门是否计时报警是一个功能,是否能录像也是一个功能,是否能自动发消息也是一个功能。这些功能甚至可能是动态配置的,如果都用多重继承来解决,那会衍生出各种各样的对象,如VideoDoor,TimeVideoDoor,MessageDoor,TimeVideoMessageDoor等等。这好像并不是什么好事。这意味着正交的程度也许还是可以提升。如果不让自己的思维局限于所有东西必须都是对象,那么就可以找到其他解法。
为了使比较更有焦点,我们先不考虑如何实现支持其他各种功能的门,仍只考虑如何为门添加定时功能。
现在我们认为定时报警功能是门的可组合部分,是使用关系而不是继承关系。也就是说,并不认为应该独立存在TimerClient这样的类,而认为这是一种偶然的组合。否则的话,XXClient这样的类会漫天飞。想象一下,门、马桶等都可以是TimerClient,也可以是Motor的Client。
修改后的Timer类像下面这个样子。

任何人使用Timer的时候,只需要提供一个触发间隔,一个回调函数,一个给回调函数用的参数。当TimedDoor需要用到Timer时它需要干的事是实现一个回调函数,并调用Register()。这时的TimedDoor像下面这个样子:

由于是静态函数,所以arglist需要用来传递当前实例的this指针。这时因为去除了TimedDoor和TimeClient的继承关系,定时这一概念和门这一概念的正交性又有所提高。另一个收获是去除了TimeClient这样一个含混的概念。而之所以这么做的原因是TimedDoor和定时器的唯一交汇点就是“定时触发某个事件这一功能”,所有其他的关联不过是人为加上去的。
如果愿意再往下走一步,就会发现TimedDoor这个类事实上也不是必须的,任何一种门,如果它有定时功能,直接实现对应的函数即可,完全不必启用继承。在这种情形下,就只剩下两个概念:Door和Timer。
3.在继承中确保正交
充分的抽象,其最终结果往往是正交的概念或逻辑,而正交的概念或逻辑大多时候是应对变化、可测试、降低耦合度的基础。这里面的一个关键点是如何看待继承,继承是OO的3大特征之一,但继承往往使正交程度减弱。
当前的主流观点是把继承等价于“IS A”这样的关系。这是对的,但范围仍然太广,有点含混,只按照这一原则行事,容易导致类的杂多化。
在上述Door的例子里,这种杂多化体现为VideoDoor、TimeVideoDoor、MessageDoor、TimeVideoMessageDoor的出现等。这并不违反“IS A”原则,所以可以用继承来实现他们,但无疑会违反正交原则或者我们将在下一节讲到的层次适度原则。
与继承相对的另一种视角是,不把Time、Video、Message视为种属上的必然差异而是视为一种偶然的可配置差异。这时候可以在Door中加入Classification这样的属性来标识不同的门。这种方法的正交性往往更好。
至于究竟在什么时候使用继承,什么时候使用属性来分类则和分类的根本原则有关。我们可以根据肤色把人分为黄色人种,白色人种,黑色人种,但不能根据人穿的衣服把人分为黄衣服的人,白衣服的人,黑衣服的人,后者一定会导致杂多的概念。分类的基础应该是尽可能本质的,恒常的特质,同时尽量避免基于偶然的,短期的特质进行分类。前者有必要使用继承,后者大多时候则可以体现为属性。
抽象与具体
在软件开发中抽象是一个经常会被用到的词,比如,有时候不好的程序会被指责为抽象不充分。
这里针对抽象和具体做一点说明。
从本质上讲,抽象是一种认清事物本质,并进行归类的过程。
与抽象相对的是具体,但抽象的来源也是具体。
假使我们需要对“人”这一名词进行定义,那么必然是要从张三,李四等具体的人身上抽取共通特征,而后才能完成定义。最终结果是“人”这一概念来源于张三,李四,但又不是张三、李四。这样一个从具体的事物中抽取共性,再进行命名的过程就是抽象。
所以人是抽象的,真实的某个人是具体的。方法论是抽象的,按照方法论来运作项目是具体的。设计和软件是抽象的,软件的使用是具体的。
这听着有点绕,我们来看个具体的例子。
排序的时候,具体的算法和待排的东西是没关系的,待排的东西只要提供比较大小的函数就可以了。
这种情况下,如果我们把排序函数写成int sort(int*p,int len);那么这里的抽象是不充分的,排序的方法和待排序的东西两者之间也是不正交的。为了进行充分的抽象,那么sort()要从具体的数据类型上解放出来:

随着抽象度的提高,适用范围确实提高了,但函数本身的可理解程度却降低了。这似乎是一对矛盾,抽象的东西灵活,用途广泛,但可理解度会有所降低,具体的东西则利弊与此相反。
4.小结
以正交性为分类的基本原则本身并没有太多值得争议之处,其关键在于认识到这是一个程度问题,而非可以完美解决的问题。在这一过程中很难追寻到彻底无瑕疵的答案,考虑到效能,正交自身则需要以合适为前提在恰当的时间点终止。
让我们以Grady Booch在《面向对象分析与设计》中的一段话来终结这一节:
那么分类为什么这么难呢?我们认为这里有两个重要原因。首先,尽管某些分类肯定比另一些分类好。但世上并不存在一个“完美”的分类。Coombs、Raffia和Thrall指出:将世界划分成对象体系的方法,其数量可能至少和完成这项任务的科学家的人数一样多。
其次,智能分类要求具有大量的创造性见解。Birtwistle、Dahl、Myhrhaug和Nygard提到:有时答案是明显的,有时却只是一种感觉,而有时候恰当的构件是分析的关键点。只有具有创造性思维的人才能找到那些在别人眼中毫无联系的事物之间的相同点。
—Grady Booch,《面向对象分析与设计》
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。
