[转]使用设计模式改善程序结构(二)

使用设计模式改善程序结构(二)

在本系列的 第一篇文章中,描述了如何通过设计模式来指导我们的程序重构过程,并且着重介绍了设计模式意图、动机的重要性。在本文中我们将继续上篇文章进行讨论,这次主要着重于设计模式的适用性,对于设计模式适用性的掌握有助于从另一个不同的方面来判断一个设计模式是否真正适用于我们的实际问题,从而做出明智的选择。

1、 回顾


在上一篇文章中,我们给出了一个使用设计模式来改善程序结构的例子,着重介绍了设计模式的意图、动机在我们程序重构过程中的指导作用。

现在,我们将关注设计模式的另一个重要方面:设计模式的适用性。解决同一个问题一般会有多种方案或者模式,但是这些模式所关注的是同一个问题的不同方面,解决不同的需求,有各自的优点和限制,各有各的解决之道。这就要求我们在选择设计模式时,对我们自己的问题有很好的理解:我们的需求是什么,我们要克服什么样的限制,我们要获得什么样的特性等等。然后,可以看看我们想使用的解决问题的设计模式是否适用于我们的问题,如果不适用,是否可以使用其他的模式来弥补,是否可以对这个设计模式进行改进使它符合我们的要求。

本文下面的部分,我们将对上一篇文章中的最终解决方案进行进一步的分析,来看看它到底满足了我们什么样的需求,又暴露了什么样的不足,最后我们会给出一个更为符合要求的解决方案。

回页首

2、 问题描述

在上一篇文章中,我们对一个网管系统中的错误处理部分的代码进行了重构,最终使用了一个visitor设计模式解决了我们的问题。细心的读者肯定会发现,这个最终方案一样存在着一个问题:如果错误的类型不是固定不变的,而是随着系统的进展不断增加的,会有什么结果呢?让我们首先来看看上一篇文章中最终的类图:

在这个类图中,我增加了几条依赖线,即ErrorHandler是依赖于DbError和CommError的。此时我们可以看到:ErrorBase依赖于ErrorHandler,ErrorHandler依赖于ErrorBase的派生类(DbError和CommError),而ErrorBase的派生类又要依赖于ErrorBase本身。这就形成了一个循环的依赖过程,这样的结果就是ErrorBase传递地依赖于它的所有派生类。

这种循环依赖关系会带来严重的问题,一旦ErrorBase新增了一个派生类,那么ErrorHandler类必须要被修改,由于ErrorBase又依赖于ErrorHandler,那么依赖于ErrorBase的所有类都需要重新编译。这就意味着ErrorBase的所有派生类以及所有这些派生类的使用者都要重新编译,这种大规模的重新编译在开发一个分布式系统时会导致非常大的工作量,因为要重新分布每一个重新编译过的类,如果在重新分布时出现一些差错(如:忘记替换一些类),就会导致微妙的错误,而且很不容易查找出来。

另外,在该模式中存在一个假设,就是默认任意一个错误处理者要处理所有的错误类型,这个假设在某些情况下是不成立的,比如:如果对于LogicError我们不打算通知LOGSys进行处理会怎样呢?我们不得不要写一个处理该错误的空函数(当然你可以在ErrorHandler中写一个缺省的实现)。如果ErrorBase的类层次架构越来越大,并且它们要求的处理方法也有很多的不同,就会导致ErrorHandler接口中的方法集庞大,并且任何一个ErrorBase的派生类的改变,都会导致大规模的重新编译(甚至是毫无关系的类也要重新编译),重新分布,如果这种改变比较频繁,结果当然是无法忍受的。

回页首

3、 问题分析

上述的问题描述暴露了visitor模式的一些使用限制,即它仅仅适用于哪些受访问的类层次架构比较固定的情况,导致这样的原因可以使用一个著名的面向对象设计原则来解释,这个原则就是:DIP(Dependence
Inversion
Principle),这个原则的核心含义是:高层模块不应该直接依赖于低层模块,所有的模块都要依赖于抽象。也就是说:容易变化的东西一定要依赖于不容易变化的东西,因为抽象的东西最不容易变化,具体的东西容易变化,所以具体应该依赖于抽象。而在visitor模式中,ErrorHandler依赖于ErrorBase的所有的具体的派生类,并且如果这些派生类容易变化的话,就会导致不能接受的结果。

通过上面的分析,可以看出打断这个循环依赖的环是克服visitor模式适用范围限制的关键。

回页首

4、 解决方案

在上述的循环依赖关系中,有两段依赖关系是无法打断的,一段是ErrorBase的所有派生类对于ErrorBase的依赖,一段是ErrorBase对于ErrorHandler的依赖,并且这两段依赖关系也是符合DIP的,那么对于仅剩的一段依赖关系我们是必须要打断的了,这段关系就是,ErrorHandler对于ErrorBase所有派生类的依赖,并且这段关系也是违反DIP的。如果我们不让ErrorHandler知道ErrorBase的派生类,那么怎样才能够针对每一个具体的ErrorBase的派生类进行相应的处理呢?

面向对象大师Robert C.
Martin给出了一个优雅的解决方案,他所采用的技巧是OO方法所建议避免使用的,RTTI(运行时类型鉴别)。可见如果RTTI运用的得当,一样可以得到很好的设计方案,并且还能够克服一些OO中多态的方法解决不了的问题(当然如果使用多态能够解决的问题,推荐还是使用多态的方法进行解决)。让我们先看看这个解决方案的类图:

通过上图可以看出,ErrorHandler中没有任何方法了,已经退化为一个空的接口,所以也就不可能再依赖于任何ErrorBase的派生类了。和visitor模式不同,这个方案中针对每一个特定的ErrorBase的派生类定义一个相应的处理接口,在每个派生类的handle方法实现中,运用RTTI技术进行相应的类型转换(把ErrorHandler转换为自己对应的错误处理接口,比如:在DbError的handle方法中,就把ErrorHandler转换为DbErrorHandler。Java在这方面做得不错,可以进行比较安全的类型转换),想要处理该错误的实体不仅要实现ErrorHandler接口,而且还要实现相应的针对具体错误类的处理接口,比如:GUISys就实现了三个接口(ErrorHandler、DBErrorHandler以及CommErrorHandler),从而也就实现了对DbError和CommError的处理。

这种方法打断了类之间的循环依赖关系,使得增加新的错误类型变得容易,并且也避免了可能出现的大规模的重新编译以及类的重新分布。并且,你也可以有选择地进行错误的处理,比如:如果GUISys不想处理DbError的话,很简单,不要实现DbErrorHandler接口即可,使得程序的结构非常清晰。可见这个方案克服了原始的visitor设计模式的不足。

通过对比上下两个类图可以看出,下面的要比上面的复杂,这也是该方案的一个缺点。如果问题的规模不大,重新编译以及重新分布没有多少工作量的话,还是可以使用原始的visitor模式的。仅仅当问题的规模扩大到visitor模式不能适用的地步时,可以考虑使用该方案。另外,由于改进的方案中使用了RTTI技术,会导致性能上的损失以及不可预测性,在使用时也要特别注意。

我们做如下的类比以便于更好地理解原始的visitor模式和改进后的方案。原始的visitor模式好像一个矩阵,在X方向上是一个个具体的错误类型,在Y方向上是一个个可以处理错误的实体,每一个交叉点上是具体的处理方法,矩阵中的每一个位置上都必须有一个处理方法。而改进后的方案象一个稀疏矩阵,仅仅在需要的位置上才有具体的处理方法,从而减少了很多冗余。

下面给出关键的代码片断:

interface ErrorBase
{
public void handle(ErrorHandler handler);
}
class DBError implements ErrorBase
{
public void handle(ErrorHandler handler) {
try {
DbErrorHandler dbHandler = (DbErrorHandler)handler;
dbHandler.handle(this);
}
catch(ClassCastException e) {
}
}
}
class CommError implements ErrorBase
{
public void handle(ErrorHandler handler) {
try {
CommErrorHandler commHandler = (CommErrorHandler)handler;
commHandler.handle(this);
}
catch(ClassCastException e)\ {
}
}
}
interface ErrorHandler
{
}
interface DbErrorHandler
{
public void handle(DBrror dbError);
}
interface CommErrorHandler
{
public void handle(CommError commError);
}
class GUISys implements ErrorHandler, DbErrorHandler, CommErrorHandler
{
public void announceError(ErrorBase error) {
error.handle(this);
}
public void handle(DBError dbError) {
/* 通知用户界面进行有关数据库错误的处理 */
}
public void handle(CommError commError) {
/* 通知用户界面进行有关通信错误的处理 */
}
}
class LogSys implements ErrorHandler, DbErrorHandler, CommErrorHandler
{
public void announceError(ErrorBase error) {
error.handle(this);
}
public void handle(DBError dbError) {
/* 通知日志系统进行有关数据库错误的处理 */
}
public void handle(CommError commError) {
/* 通知日志系统进行有关通信错误的处理 */
}
}

回页首

5、 结论

本文从另一个方面,设计模式的适用性,探讨了在进行程序重构时该如何选择重构的目标,以及如何对现有的设计模式进行改进使之符合我们的目标。本文的主要目的是想说明,在进行设计模式的选择时,不仅要关注设计模式是解决什么问题的,还要关注使用该设计模式解决问题时会受到什么约束和限制。这样就可以帮助你更好地理解问题,做出合理的取舍,或者你可以根据自己的需求对已有的设计模式进行修改使之满足你的要求,如果你这样做了,你就很可能创造出了一种新的设计模式。

参考资料

[1] Design Patterns, Gamma, et. al., Addison Wesley

[2] Design Principles and Design Patterns,Robert C.
Martin,www.objectmentor.com

[3] Refactoring To Patterns, Joshua Kerievsky, industriallogic.com

[4] The Visitor Family of Design Patterns,Robert C.
Martin,www.objectmentor.com

[转]使用设计模式改善程序结构(二),布布扣,bubuko.com

时间: 2024-10-16 22:38:34

[转]使用设计模式改善程序结构(二)的相关文章

[转]使用设计模式改善程序结构(三)

使用设计模式改善程序结构(三) 设计模式在某种程度上确实能够改善我们的程序结构,使设计具有更好的弹性.也正是由于这个原因,会导致我们可能过度的使用它.程序结构具有过度的.不必要的灵活性和程序结构没有灵活性一样都是有害的.本文将分析过度的灵活性可能造成的危害,并且结合一些实例来阐述使用设计模式改善程序结构应遵循的原则. 1. 介绍 本系列文章的前两篇主要讲述了如何使用设计模式来改善我们的程序结构,大家可以看到经过调整的代码具有了更大的弹性,更容易适应变化.读者朋友可能也具有类似的经验,通过使用设计

[转]使用设计模式改善程序结构(一)

使用设计模式改善程序结构(一) 设计模式是对特定问题经过无数次经验总结后提出的能够解决它的优雅的方案.但是,如果想要真正使设计模式发挥最大作用,仅仅知道设计模式是什么,以及它是如何实现的是很不够的,因为那样就不能使你对于设计模式有真正的理解,也就不能够在自己的设计中正确.恰当的使用设计模式.本文试图从另一个角度(设计模式的意图.动机)来看待设计模式,通过这种新的思路,设计模式会变得非常贴近你的设计过程,并且能够指导.简化你的设计,最终将会导出一个优秀的解决方案. 1.介绍 在进行项目的开发活动中

C#学习笔记二:C#程序结构

从最简单的HelloWorld开始入手,这是一个最低限度的C#程序结构. C# Hello World 示例 一个C#程序主要由以下几部分组成: 命名空间声明 一个类 类方法 类属性 一个Main方法 语句和表达式 注释 先看看下面的示例,将打印字的简单的代码 "Hello World": using System; namespace HelloWorldApplication { class HelloWorld { static void Main(string[] args)

[C++基本语法:从菜鸟变成大佬系列](二):C++的程序结构

C++程序结构 让我们看一下打印Hello World这个词的简单代码. 1 #include <iostream>//头文件名,iostream表示有输入输出流 2 using namespace std; 3 // main() 是主程序开始的地方 4 int main() { 5 cout<<"Hello World"; // 输出Hello World 6 return 0; 7 } 让我们看一下上述程序的各个部分 C ++语言定义了几个标题,其中包含对

设计模式大总结(二)

上篇博客给大家介绍了六大原则和设计模式之间的关系,以及创建型模式和结构型模式 (http://blog.csdn.net/zhangzijiejiayou/article/details/32712779).本文将给大家介绍行为型模式. 行为型模式 是对在不同的对象之间划分职责和算法的抽象化. 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到 通知并被自动更新. 优点:在解耦合,让耦合的双方都依赖于抽象的接口而不是具体,从而使各自的变化都不会影响

设计模式大类--结构模式(上)

大概有7中结构模式,分为上下两篇.一.Adapter(适配器)描述:将两个不兼容的类结合一起使用,一般需要用到其中某个类的若干方法好处:在两个类直接创建一个混合接口,而不必修改类里面的其他代码 例子:假设我们要打桩,有两种类:方形桩 圆形桩.public class SquarePeg{ public void insert(String str){ System.out.println("SquarePeg insert():"+str); } } public class Roun

黑马程序员——程序结构

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 一.程序的结构分类 1.顺序结构     “直流型” 2.分支结构     “岔路型” 3.循环结构 二.顺序结构 顺序结构是3种结构中最简单的,也是默认的流程结构:程序中的语句是按照书写顺序执行的. 三.分支结构 1.if语句 C语言的if语句有三种基本形式. 1)第一种形式: if(表达式) {语句}: 原理:如果表达式的值为真,则执行其后的语句,否则不执行该语句.例如: int a =

计算机病毒的定义、特征、程序结构、命名、传播与生命周期

一.定义:凡是人为编制的,干扰计算机正常运行并造成计算机软硬件故障, 甚至破坏计算机数据的可以自我复制的计算机程序或者指令集合 都是计算机病毒. 二.特征:非法性.隐藏性.潜伏性.可触发性.表现性.破坏性.传染性. 针对性.变异性.不可预见性. 隐藏性:缩小体积.潜入系统目录.标记坏簇.系统漏洞. 潜伏性:依附宿主程序伺机扩散. 破坏性:良性病毒.恶性病毒. 不可预见性:病毒超前于反病毒产品. 三.计算机病毒的程序结构 1> 引导部分:将病毒主题加载到内存,为传染部分做准备. 2> 传染部分:

JavaWeb-07(tomcat与web程序结构与Http协议与Servlet基础)

JavaWeb-07 JavaWeb-tomcat与web程序结构与Http协议与Servlet基础 HTTP协议(记住) 1.http协议:规定了客户端和服务端交流时的数据格式 a. WEB浏览器与WEB服务器之间的一问一答的交互过程必须遵循一定的规则,这个规则就是HTTP协议. b. HTTP是HyperText Transfer Protocol(超文本传输协议)的简写,它是TCP/IP协议的一个应用层协议,用于定义WEB浏览器与WEB服务器之间交换数据的过程及数据本身的格式. c. HT