在设计刚开始的时候,没有人知道ConcreteComponent最后的发展会是什么样。很明显,这是一个处于不确定环境中的设计,我们唯一能够确定的,只有Component这个类体系一定会拥有Operate这个方法,所以,我们设计了一个接口Component来约束类体系,要求所有的子类都拥有Operate方法。另一个目的是为客户端调用提供了统一的接口,这样,客户端对服务端信息的了解到了最小的程度,只需要知道Operate这个方法,并选择适当的类就可以了。还可以对这个模型做进一步的改进,令耦合程度进一步降低。
在统一了接口之后,我们就可以根据需要来实现现有的功能,我们实现了一个ConcreteComponent类,它实现了Component接口,并实现了核心的功能。如果在未来,需求的变化,要求我们增加额外的行为,我们就使用ConcreteDecorator类来为ConcreteComponent添加新的功能:
先找出共通点,然后实现共通点,并把不确定的信息设计为扩展,这就是推迟决策的设计思路。但是,应该指出的是,上面这个例子的设计,仍然有很多的限制,例如,增加的需求(也就是某个ConcreteDecorator)中可能拥有新的接口,例如需要一个AnotherOperate方法,这时候,原先的扩展性设计就又变得难以满足需要了。在软件设计中,针对接口设计的灵活性和扩展性虽然比以往的设计增强的许多,但它并不是功能较多的,而且取决于设计师对需求的理解能力和设计水平。此外,推迟设计决策要求我们学习抽象的思维,识别和区分软件中变化和不变的部分。
注重接口,而不是注重实现
Martin Fowler把软件设计分为三个层面:概念(conceptual)层面、规约(Specification)层面、实现(Implementation)层面。软件的设计应该尽可能地站在概念、规约层面上进行,而不是过分关注实现层面。之所以有时候我们发现在迭代的过程中,软件难以承受这种变化,那么,很大的可能是规约层面和实现层面出了问题。我们在前面一节讨论重构和审查的时候说,消除重复代码是一项复杂的工作,针对规约设计就是其中最有效,但也是最难的一种方法。
我们可以把规约层面想象为软件的接口或是抽象类,或是具体类的公有方法,而把实现层面想象为实现类、实现细节。那么,我们的原则应该尽可能设计稳定的规约层面,并为客户(可能是真正的客户,大部分情况下是使用你的代码的客户程序员)提供一个优秀的、简单的界面(接口)。社会发展到现在的水平,任何一个人都不会花费过多的时间来研究你的代码,如果你的代码不能够为他人提供便利性,那么最后被淘汰的一定就是你的代码。Java语言的成功,很大程度上就在于他在保证其强大功能的同时,还提供了一个简单、易用、清晰的规约界面。
在软件设计中,重视规约层面的设计是很普遍的。为什么我们提倡三层架构的软件设计?最重要的是因为他为软件结构合理性贡献巨大,远远超过了他的其它价值。在现代的软件设计中,数据库、界面、业务建模其实是三种差异较大的技术,这就导致了三者的变化度是不同的。根据区分不同变化度的原则,我们知道,必须对三种技术进行区分。而这正是三层架构的主要思路。从这个思路扩展出去,我们还可以根据变化度的需要,将三层架构演变为四层架构、甚至多层架构。而多个层次之间,正是通过优秀的规约界面来达到最松散的耦合的。
在精益编程中,为了避免浪费,要求每位程序员提高代码的规约层面的稳定性是非常有必要的。一个系统中,设计优良的规约界面能够拥有比较好的抗变化能力,能够较好的适应迭代过程。
回归
版本2的软件出现了版本1中不存在的行为,称为回归。回归是软件开发中的主要问题。在对现有功能修改的同时影响原有的行为,这是造成bug的主要原因。在迭代的过程中,必须避免回归行为的出现。而避免回归问题的主要解决方法是构建自动化的测试,实现回归测试。
成功构建回归测试的关键仍然在于是否能够设计出优秀的规约界面,并针对规约界面进行测试。这样,不但设计具有抗变化性,测试同样具有抗变化性。而唯一可能改变的就只有实现了。在回归测试的帮助下,代码的变化是不足为惧的。我们把有关测试的详细讨论放在测试一节中。
组织规则
在后续的章节中,我们会详细的讨论XP中的一项非常有特点的组织规则-结对编程。这里我们需要知道,不同的团队有着不同的组织,其迭代过程也需要应用不同的组织规则。例如,组织的规模,小规模的组织可以应用更快的迭代周期,如一周,在一个迭代周期中,团队可以集中力量来开发一个需求,强调重构和测试,避免过多的前期设计。对于大的组织来说,可以考虑迭代周期更长一些,更注重前期设计,并将开发人员和测试人员的迭代周期交错开来。团队的组织构成也是影响迭代过程的主要原因。团队是否都是由相同水平的人构成,每个人的专长是否能够互补,团队是否存在沟通问题。