设计模式在数据存储设计中的应用
2013-04-25张欲蓉
张欲蓉,严 华
(中国电子科技集团公司51所,上海 201802)
0 引 言
数据存储是各种计算机应用系统的一个重要功能。几乎所有的计算机应用系统都需要将各种数据信息存储为不同格式的文件。在存储过程中,数据导出与数据保存的代码很容易高度耦合,很难分别对数据导出与数据保存部分单独进行修改、扩充与重用。本文所述的数据存储设计,运用bridge和decorator模式,将数据导出与数据保存解耦,使设计易维护、易扩展、易复用。
1 问题分析
在某个典型的电子对抗领域的应用中,有上级指控命令、信号分选结果、系统工作日志等数据需要存储,这些数据可能需要被存储为多种格式的文件,如*.xls、*.txt、*.html或*.mdb等。实现数据存储功能的最常见的2种设计如下:
设计1:设计一个模块,让其提供多个接口来满足需要,这种设计缺点很明显,降低了代码的内聚性,使之趋于不良耦合,可重用性和可靠性大打折扣。
图1 设计2的设计类图
根据这种设计,假设系统数据的种类为M,文件格式的种类为N,则一共要设计M×N个类才能满足要求。这种设计存在如下问题:(1)每增加一种待导出的数据或增加一种文件导出格式,需要增加多个类;(2)如果对待导出数据或文件的保存格式进行修改将影响与之相关的多个类;(3)随着M或N值的增加,最终设计类的个数会增长为不可控制的庞然大物。
本文所要解决的问题就是提出一种设计,这种设计能减少类的数量,降低类间的耦合度,保持对象的内聚性,易理解,易管理,减少变化带来的影响,提高重用性[1]。
2 系统设计
软件设计真正要做的许多内容,就是发现职责并把职责相互分离[2]。将待导出的数据视为数据源,将数据存储的过程分解为导出和保存2步:导出的职责是按需求过滤、提取、组织数据,保存的职责是将组织好的数据保存为不同格式文件。经分解后2个职责都比较单一,更能适应变化的发生。数据存储的过程如图2所示。
图2 数据存储示意图
2.1 类的设计
设计类图见图3。
图3 设计类图
这里设计了2个抽象基类:CExport和CSave。CExport提供数据导出的接口,CSave提供数据保存的接口。CDB代表数据源的抽象,设计时应让其提供接口,使其他对象能够获取数据源的数据。
CExport派生出CExport Cmd、CExportSignal和CExport Log等子类,CExportCmd子类负责导出上级指控命令,CExportSignal负责导出信号分选结果,CExport Log负责导出系统工作日志。
CSave派生出 CSaveXls、CSave Txt、CSave Html和CSave Mdb等子类,这些子类分别负责将导出的数据保存为xls、txt、html和mdb等格式。
有些数据文件可能需要装饰,例如:数据文件需要增加标题或结尾,于是设计1个抽象类CDecorat-or Export,它是CExport的子类,用于提供统一的装饰接口。从CDecorator Export派生出2个子类:CDecorator Export Topic和CDecorator Export Tail。CDecorator Export Topic用于增加标题,CDecorator Export Tail用于增加结尾。如果需要实现其他装饰功能,从CDecorator Export类派生出相应装饰作用的类即可。
2.2 实现数据导出和数据保存的分离
按照图1的设计,每增加一种待导出数据或文件导出格式,就需要增加多个类,最终将导致类的数量庞大。且一旦有变化发生,将引起多个类一起变化。究其原因,就是设计类职责过多,耦合过于紧密。紧密耦合导致脆弱的设计,当变化发生时,设计不能适应这种变化。事实上,完全可以找出哪些是数据导出,哪些是数据保存,然后进行职责分离,使设计符合单一职责原则[2]。
e.g. A: The dinner is ready. Tim has not made it.Can anybody give him a call?
软件设计的一个重要原则是依赖倒置原则:高层模块不应该依赖低层模块,2个都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。针对接口编程,不要对实现编程。相对于实现细节的多变性,抽象的接口要稳定得多。依据这个原则,设计抽象类CSave和CExport,CExport提供数据导出的接口;CSave提供数据保存的接口。让CExport基类维持一个指向CSave类的指针。这样数据导出CExport的各个子类对数据保存的依赖关系都终止于抽象类或接口。正是由于这个抽象的依赖关系,以及子类型对父类型的可替换,实现了数据导出和数据保存的分离。设计类图见图4,这种设计是bridge模式的一个应用。
图4 应用bridge模式实现数据导出与数据保存的分离
CExport的子类CExportSignal、CExportCmd等和CSave的子类CSaveXls、CSave Html等之间没有固定的绑定关系,可以在程序运行过程中根据需要选择或切换。
下面以将信号分选结果导出保存为Txt和Html为例进行说明:
//对数据保存的依赖终止于抽象类CSave,可传入指向CSave的子类的指针,将数据保存为不同格式文件。
实现数据导出与数据保存功能的客户代码如下:
//传入指向CSave Txt类的指针,实现子类对父类的替换,将信号分选结果导出保存为txt文件。
m_p Export->Export(m_pSave Txt);
//传入指向CSave Html类的指针,实现子类对父类的替换,将信号分选结果导出保存为html文件,不需要对CExportSignal作修改。
m_pExport->Export(m_pSave Html);
采用bridge模式的核心意图,就是将数据保存独立出来,减少与数据导出的耦合,让它与数据导出各自独立地变化,并且该变化对其他部分不会产生影响。
CExport的各个子类只负责按需求过滤、提取、组织数据,将保存功能分配给了CSave的各个子类,但是它们能够共同协作并提供某种良好界定的行为[3],因此它们具有高内聚的性质。每增加一种待导出数据,只需要设计一个从CExport派生的子类即可,其他所有的类均不需要任何改变。同样,每增加一种文件导出格式,只需要增加一个从CSave派生出的子类即可,其他所有类也不需要改变。职责的合理分配与依赖倒置原则的应用,降低了CExport的子类和CSave的子类之间的耦合性,保持了对象的内聚性。
CSave Txt、CSave Html、CSaveXls及 CSaveDoc等承担保存文件职责的类经过测试成熟后,便可被其他项目所复用。
2.3 数据导出功能的动态增加
导出日志时,有时需要加上导出日期、日志名称等信息,或是添加在标题处,或是添加在结尾处,有时也可能并不需要增加任何装饰。即装饰功能是动态增加的。如何实现这个动态装饰功能呢?常见的方法是在数据导出类中增加接口。但是这样就必须修改各个数据导出类的接口,增加新的逻辑,导致原有类的复杂度增加,客户代码也需要适应这个新的逻辑和接口,一起作修改。当每次增加新的装饰功能时,修改又会再次发生在同样的地方。在设计中应用了decorator模式来隔离这些变化,使变化产生的影响降到最低。
软件设计应遵循的另一个原则是:开放-封闭原则,就是说软件实体(类、模块、函数等等)应该可以扩展,但是不可修改[2]。即对于扩展是开放的,对于更改是封闭的。但是无论模块设计得多么“封闭”,都会存在一些无法对之封闭的变化。既然不可能完全封闭,就创建抽象来隔离变化,即找出变化并封装变化[4]。这里的变化就是动态增加的装饰功能。设计的出发点就是让CExport类及其子类无需知道装饰功能的存在,即不对CExport类及其子类作任何修改,保证它们封闭性的同时,提供扩展的开放性。设计类图见图5,这种设计是decorator模式的一个应用。
图5 应用decorator模式实现数据导出的动态增加
设计CDecorator Export类,它是用于装饰的抽象基类,它还是CExport的子类,与被装饰的CExport及其子类具有相同的接口。它同时维持一个指向CExport对象的指针,并改写了从CExport继承来的Export接口,改写的示例代码如下。这为在不改变CExport对象的情况下给CExport对象增加职责提供了基础。
将CExport对象看作一个核,CDecorator Export对象看作它的外壳,通过套上外壳可以改变核的行为。子类CDecorator Export Topic和CDecorator Export Tail从CDecorator Export派生,将它们实例化后的对象就是核的外壳。它们提供用于装饰的接口,给CExport对象添加新的职责,一个负责增加标题,一个负责增加结尾。它们均改写了从CDecorator Export继承过来的Export接口,实现装饰功能。示例代码如下:
如果需要实现其他装饰功能,只需增加一个从CDecorator Export派生出来的装饰类即可,原有的实现数据导出与数据保存功能的类不需要作任何改动。对于客户代码来说,通过与2.1中示例代码的比较可知,只有实例化CExport类为对象这一个地方有改动,由于装饰类是CExport的子类,它们具有相同接口,客户代码的其余地方均不需要改动。
3 其他
为了增加灵活性和封装变化,本文在设计中选择了细粒度的类,每个类都有比较明晰、单一的职责。使用这些细粒度类提供了更为灵活的策略,也提供了更多的可复用性。
在应用中有一点要特别注意,由于装饰对象的接口与它所装饰的CExport对象的接口是一致的,保持CExport类的简单性很重要,否则会使装饰类拥有一些它们并不需要的功能而难以使用,也使系统付出不必要的代价。设计时,CExport类应集中于定义接口,尽量少地维护数据,对数据的表示和维护应延迟到其子类中。当CExport类很庞大时,需要考虑使用别的方法来动态增加装饰功能。
4 结束语
设计模式的使用有效地提高了设计的质量,使整个设计具有清晰的结构、良好的扩展性和易复用性。本文提出的数据存储设计已成功运用于多个项目,对于缩短项目开发周期具有积极的意义。
[1]Craig Larman.Applying UML and Parrerns[M].北京:机械工业出版社,2006.
[2]Martin Robert C.敏捷软件开发:原则、模式与实践[M].北京:清华大学出版社,2003.
[3]Booch.Object Solutions:Managing The Object-Oriented Project[M].New York:Addision-Wesley,1996.
[4]Alan Shalloway,Trott James R.Design Patterns Explained:A New Perspective on Object-oriented Design[M].徐言声译.北京:人民邮电出版社,2006.
[5]Erich G,Richard H,Ralph J,et al.Design Patterns:Elements of Reusable Object-oriented Software[M].北京:机械工业出版社,2005.