设计模式 学习笔记 之三
第3章 开放封闭原则
开放封闭原则(Open-Closed Principle)可以被表述如下:软件实体(类,模块,函数,等等)应该对扩展开放,对修改封闭。
开放封闭原则的要义是:软件应该能够在不修改或尽量少修改现在有源代码的情况下,实现易扩展性。
实际上,基于共性变性分析所获得的设计就是遵守开放封闭原则的。例如,对于前一章的Student类的例子,如果我们现在需要按CSV格式产生学生信息报告,那么只需要引入一个新的CSVReportGenerator类,而不用修改原有的类。同样地,如果我们需要把前一章中的Employee类存储到SQLServer数据库中,我们也只需要引入一个新的SQLServerDB类,而无需修改其它的类。
要实现开放封闭原则,除了上面所应用的共性变性分析技术之外,还有另外一种非常重要的技术:数据驱动法(Data-driven method),也称为表驱动法(Table-driven method)。数据驱动法的目的是将通用逻辑与具体数据相分离。具体数据被存放在一个表当中,供通用逻辑使用;而通用逻辑并不关心数据表中到底存放了什么具体数据,它对任何数据都执行相同的逻辑。通过将逻辑与数据相分离,逻辑实现了对修改封闭,而数据表却可以在需求发生变化时通过扩展或替换来改变系统的行为。换言之,我们只需改变数据表中存放的数据就能扩展我们的系统,而无需去修改容易被改错的逻辑,这也就实现了开放封闭原则。下面我们来看一个具体的例子。
例子:灵活消息格式
假设我们要编写一个程序来打印存储在一份文本文件中的消息。通常该文件会存储约500条消息,而消息的种类有大约20种。这些消息源自于一些浮标,提供有关水温、浮标位置等信息。
每一条消息都有若干字段,每个字段的类型可能是int,double,String或者Date中的一种。每条消息由一个消息ID开头,消息ID是一个标明消息种类的正整数。下面显示了这个文件的内容和一些具体的消息格式。
让我们首先应用基本的面向对象的技术来对这个问题建模,得到下面这个方案。
这个模型应该说是不错的。它的核心就是共性变性分析。这里的共性是“从消息文件中读取一条消息,并打印它”;而变性则是“消息的不同种类”。这个模型满足了开放封闭原则吗?看上去是满足了。当有新的消息种类要引入时,我们只需要新加入一个Message接口的子类即可,程序的其它部分完全不需要修改。
如果我们止步于这个设计,那么这样的设计只能打50分。为什么?因为在这个设计中,我们还少分析了另外一个共性:每种消息的字段都属于几种数据类型中的某一种,而这每种字段数据类型在从文件读取及打印时的处理方式是相同的。分析到了这一点,那么再次应用共性变性分析,我们不难得到新的设计。
这个新设计比前一个更好一些,因为它比前一个设计多利用了一个共性,这就更增加了设计应对变化的能力。不难看出,在新的设计中,当要引入新的消息格式时,只需要从Message抽象类派生出一个新的子类,并在它的构造函数中调用appendField()来说明它的格式即可,而无需再去覆盖read()和print()方法了,因为这些共同的逻辑已经被上移到Message抽象类当中了。与此同时,程序中的其它类无需作任何变动。当要引入新的字段数据类型时,情况也是类似的,只需从FieldType接口派生出子类,而无需修改其它类。开放封闭原则得到了满足,一切看起来很好!
如果我们止步于这第2个设计,那么这个设计可以打80分,但是它仍然具有改进的余地。为什么?分析一下这第2个设计方案,可以看出:实际上现在Message的每个子类的作用就是描述消息的格式(通过其构造函数),除此之外没有其它作用。既然这样,那么我们可以再进一步,直接把对消息格式的描述,存放到一个表中,而让MsgFileReader以消息ID为索引去查询这个表,得到特定消息所对应的消息格式描述,再按照消息描述来读取消息并打印。
这个设计的好处是什么?首先,它保留了第2个方案的优点,即无需针对每种消息格式都重新定义read()和print()方法。但同时,它又比第2个方案更简单,通过引入一个表,我们去除了原先的Message类层次结构,降低了复杂性。当有新的消息格式引入时,我们只需要修改构造msgDescriptorTable的方法就行了。
总结一下,在这个例子中,存在两个共性变性分析,其中一个更明显,而另一个不那么明显。仅仅利用明显的共性变性分析,我们可以得到尚可将就使用的设计。但如果把两个都分析到的话,那就能得到一个优良的设计。但从第2个设计方案中,我们可以获得启发,并进一步应用数据驱动法把逻辑与数据相分离,最终得到一个优雅的设计。
软件的部署期定制
前面的讲的共性变性分析和数据驱动法都是在软件开发期遵循开放封闭原则的技术。但是,考虑一下下面的情况:假如你已经开发出了前一章提到的学生信息数据库系统,并且已经获得了用户的订单。但是,有的用户要求使用纯文本格式生成学生信息报告,而有的用户要求使用XML格式生成学生信息报告。如果你的软件是把ReportGenerator的构造硬编码到源代码中,那么你需要做两次编译,得到不同客户需要的不同版本,才能满足用户的需要。这看上去不怎么灵活。
没有更好的办法吗?难道不能在软件安装或部署时定制软件的行为吗?当然可以!大多数软件采用的方法是读取配置文件来定制软件的行为。实际上,实现软件的部署期定制也是开放封闭原则的体现。由于软件一旦部署到用户环境中就无法修改其源代码,更不用说重新编译了,这时通过修改配置文件,我们却可以控制或扩展软件的行为。
使用配置文件实现部署期定制背后的关键思想是延迟数据的绑定期,即:并不是把数据的设置硬编码到源程序中(这称为编译期绑定),而是在软件运行之后通过读取配置文件来设置(这称为运行期绑定)。通过数据的运行期绑定,我们可以让软件的行为在运行时灵活地变化。
例子:灵活消息格式(续)
仍以本章的灵活消息格式为例。在前面的第3个设计中,我们是把msgDescriptorTable硬编码到源代码中。这样,当加入新的消息格式或对现有消息的格式作出修改时,必须修改源代码,并重新编译,再重新部署。这很费劲,不是吗?而一个更好的策略是:把对每个消息种类的格式描述都放到一个配置文件中。当程序开始运行时,从配置文件中读取每个消息种类的格式描述信息,动态地构建出msgDescriptorTable。这样,已部署的程序无需修改,只需修改配置文件,再重启程序,就行了。多么简单!