八、正交性
在计算技术中,该术语表示某种不相依赖性或是解耦性。如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。在设计良好的系统中,数据库代码与用户界面是正交的;你可以改动界面,而不影响数据库;更换数据库,而不用改动界面。
当任何系统高度依赖时,就不再有局部修正(local fix)这样的事情。
消除无关事物之间的影响。我们想要设计自足的组件:独立、具有单一、良好定义的目的。如果组件是相互隔离的,你就知道你能够改变其中之一,而不用担心其余组件。只要你不改变组件的外部接口,你就可以放心:你不会造成波及整个系统的问题。
如果你编写正交系统,你得到两个主要好处:提高生产率与降低风险。
提高生产率
- 改动得以局局部化,所以开发时间和测试时间得以降低。与编写单个的大块代码相比,编写多个相对较小的、自足的组件更为容易。你可以设计、编写简单的组件,对其进行单元测试,然后把它们忘掉——当你增加新代码时,无须不断改动已有的代码。
- 正交的途径还能够促进复用。如果组件具又明确而具体的、良好定义的责任,就可以用其最初的实现者未曾想象过的方式,把它们与新组件组合在一起。
- 如果你对正交的组件进行组合,生产率会有相当微妙的提高。假定某个组件做M件事情,而另一个组件做N件事情。如果它们是正交的,而你把它们组合在一起,结果就能做N×N件事情。但是,如果这两个组件是非正交的,它们就会重叠,结果能做的事情就更少。通过组合正交的组件,你的每一份努力都能够得到更多的功能。
降低风险
- 正交的途径能降低任何开发中固有的风险。
- 有问题的代码区域被隔离开来。如果某个模块有毛病,它不大可能把病症扩散到系统的其余部分。要把它切掉,换成健康的新模块也更容易。
- 所得系统更健壮。对特定区域做出小的改动与修正,你所导致的任何问题都将局限在该区域中。
- 正交系统很可能得到最好的测试,因为设计测试、并针对其组件进行测试更容易。
- 你不会与特定的供应商、产品、或是平台紧绑在一起,因为与这些第三方组件的接口将被隔离在全部开发的较小部分中。
工作中应用正交的几种方式
项目团队
你是否注意到,有些项目团队很有效率,每个人都知道要做什么,并全力做出贡献,而另一些团队成员却老是在争吵,而且好像无法避免互相妨碍?
这常常是一个正交性问题。如果团队的组织有许多重叠,各个成员就会对责任感到困惑。每次改动都需要整个团队开一次会,因为他们中的任何一个人都可能受到影响。
怎样把团队划分为责任得到良好定义的小组,并使重叠度降至最低呢?没有简单的答案。这部分地取决于项目本身,以及你对可能变动的区域的分析。这还取决于你可以得到的人员。我们的偏好是从使基础设施与应用分离开始。每个主要的基础设施组件(数据库、通信接口、中间件层等等)有自己的子团队。如果应用功能的划分显而易见,那就按此划分。然后我们考察我们现有的(或计划有的)人员,并对分组进行相应的调整。
你可以对项目团队的正交性进行非正式的衡量。只要看一看,在讨论每个改动时需要设计多少人。人数越多,团队的正交性就越差。显然,正交的团队效率也更高(尽管如此,我们还是鼓励子团队不断地相互交流)。
设计
大多数开发者都熟知需要设计正交的系统,尽管他们可能会使用像模块化、基于组件、或是分层这样的数据描述该过程。系统应该由一组相互协作的模块组成,每个模块都实现不依赖于其他模块的功能。有时,这些组件被组织为多个层次,每层提供以及抽象。这种分层的途径是设计正交系统的强大方式。因为每层系统都只使用在其下面的层次提供的抽象,在改动底层实现,而又不影响其他代码方面,它拥有极大的灵活性。分层也降低了模块间依赖关系失控的风险。
对于正交设计,有一种简单的测试方法。一旦设计好组件,问问你自己:如果我显著地改变某个特定功能背后的需求,有多少模块会受影响?正交系统中,答案是“一个”。移动GUI面板上的按钮,不应该要求改动数据库schema。增加语境敏感的帮助,也不应该改动记账子系统。
让我们考虑一个用于监视和控制供暖设备的复杂系统。原来的需求要求提供图形用户界面,但后来需求被改为增加语音应答系统,让按键电话控制设备。在正交地设计的系统中,你只需要改变那些与用户界面有关联的模块,让它们对此加以处理:控制设备的底层逻辑保持不变。事实上,如果你仔细设计你的系统结构,你应该能够用同一个底层代码支持库支持这种界面。
还要问问你自己,你的设计多大程度上解除了与现实世界中的变化的耦合?你在把电话号码当作顾客标识吗?如果电话公司重新分配了区号,会怎么样?不要依赖你无法控制的事物属性。
工具箱与库
在你引入第三方工具箱和库时,要注意保持系统的正交性。要明智地选择技术。
在引入某个工具箱时(甚至是来自你们团队其他成员的库),问问你自己,它是否会迫使你对代码进行不必要的改动。如果对象持久模型(object persistence schema)是透明的,那么它就是正交的。如果它要求你以一种特殊的方式创建或访问对象,那么它就不是正交的。让这样的细节与代码隔离具有额外的好处;它使得你在以后更容易更换供应商。
编码
每次你编写代码,都降低应用正交风险性的风险。除非你不仅时刻监视你正在做的事情,也时刻监视应用的更大语境,否则,你就有可能无意中重复其他模块的功能,或是两次表示已有的知识。
你可以将若干技术用于维持正交性:
- 让你的代码保持解耦。编写“羞怯”的代码——也就是不会没有必要地向其他模块暴露任何事情、也不依赖其他模块的实现的代码。如果你需要改变对象的状态,让这个对象替你去做。这样,你的代码就会保持与其他代码的实现的隔离,并增加你保持正义的机会。
- 避免使用全局数据。每当你的代码引用全局数据时,它都把自己与共享该数据的其他组件绑在一起了。即使你只想对全局数据进行读取,也可能会带来麻烦(例如,如果你突然需要把代码改为多线程)。一般而言,如果你把所需的任何语境(context)显式地传入模块,你的代码就会更易于理解和维护。在面向对象应用中,语境常常作为参数传给对象的构造器。换句话说,你可以创建含有语境的结构,并传递指向这些结构的引用。
《设计模式》一书中的Singleton(单例)模式是确保特定类的对象只有一个实例的一种途径。许多人把这些Singleton对象用作某种全局变量(特别是在除此而外不支持全局概念的语言中,比如Java)。使用Singleton要小心——它们可能造成不必要的关联。 - 避免编写相似的函数。你常常会遇到看起来全都很像的一组函数——它们也许在开始或结束处共享共享公共的代码,中间的算法却各有不同。重复的代码时结构问题的一种症状。要了解更好的实现,参见《设计模式》一书中的Strategy(策略)模式。
养成不断批判对待自己的代码的习惯。寻找任何重新进行组织、以改善其结构和正交性的机会。
测试
正交系统和实现的系统也更易于测试。因为系统的各组件间的交互式形式化的和有限的,更多的系统测试可以再单个的模块级进行。这是好消息,因为与集成测试(integration testing)相比,模块级(或单元)测试要更容易规定和进行得多。事实上,我们建议让每个模块都拥有自己的、内建在代码中的单元测试,并让这些测试作为常规构建过程的一部分自动运行。
构建单元测试本身就是对正交性的一项有趣测试。要构建和链接某个单元测试,都需要什么?只是为了编译或链接某个测试,你是否就必须把系统其余的很大一部分拽进来?如果是这样,你已经发现了一个没有很好地解除与系统其余部分耦合的模块。
修正Bug也是评估整个系统的正交性的好时候。当你遇到问题时,评估修正的局部化程度。
你是否只动了一个模块,或者改动分散在整个系统的各个地方?当你做出改动时,它修正了所有问题,还是又神秘地出现了其他问题?这是开始运用自动化的好机会。如果你使用了源码控制系统,当你在测试之后,把代码签回时,标记所做的bug修正。随后你可以运行月报,分析每个bug修正所影响的源文件数目的变化趋势。
文档
也许会让你惊讶,正交性也适用于文档。其坐标轴是内容和形式表示。对于真正正交的文档,你应该能显著地改变外观,而不用改变内容。
认同正交性
运用DRY原则,你是在寻求使系统中的重复降至最小;运用正交性原则,你可降低系统的各组件间的相互依赖。这样说7也许有点笨拙,但如果你紧密结合DRY原则、运用正交性原则,你将会发现拟开发的系统会变得更为灵活、更易于理解、并且更易于调试、测试和维护。、
如果你参加了一个项目,大家都在不顾一切地做出活动,而每一处改动似乎都会造成别的东西出错。项目很可能没有进行正交的设计和编码,是时候重构了。