设计模式初学者系列-策略模式
-------为什么总是继承
模板方法的延续
这篇稿子是基于我的前一篇模板方法设计模式之上演绎的,如果没有阅读请点击这里查看,以了解这篇稿子的上下文。
在模板方法设计模式里我举了一个例子:教育部规定了新生报到流程的算法骨架,然后这个算法骨架中的一些关键步骤由各高校自由的去发挥。我在这个例子中将高校设为一个抽象类,各高校要实现的算法步骤都是抽象方法。我还给出了两个高校的实现代码:清华大学和北京大学。在这个例子中本没有什么问题,但是软件总是会变的。
当有更多的高校要实现的时候,我们就会发现,很多高校的有些报到步骤实现是一样的,这就存在子类中有大量的重复代码,重复总是会出问题的。当然我们可以使用Martin Fowler的Pull Up Method(Refactoring P320)重构方法,将这些共同的部分推移到高校这个父类实现,并将这个抽象类改为virtual。
public abstract class 高校
{
public void 报到()
{
教务处报到();
缴费();
本院系报到();
教材科发教材();
}
protected abstract void 教务处报到();
//方法由抽象的更改为虚方法
protected virtual void 缴费()
{
//将这个方法在父类去实现,因为好多高校的实现都是这样的,避免重复
}
protected abstract 专业等信息 本院系报到();
protected abstract 教材 教材科发教材();
}
但是,现在出现了这样的情况:A,B,C等几个大学的实现在某些步骤上有些相同,D,F在某些步骤的实现有些相同,也许你会说:这不好办,继续使用继承呗,将共同的东西往上推,并且在“高校类”和各高校实现的类中间插入一些类,这些类将提供共同的实现。好像是个很好的办法。来瞧一瞧:
重复的代码确实减少了很多,但是还有一些重复(心里在默默的骂道:TMD,为什么C#不支持多继承,不然我就可以消除重复了),也许你还在自我陶醉的欣赏着自己多么完美的类继承层次,在那里感慨OO的强大。但是随着具体的高校越来越多,而且有的高校的报到步骤居然要发生改变,你小心的在中间那一层添加新的类,并将一些高校的实现转移,每一次你都非常小心(这个系统正在高速的运转,每改错一步,就有多少莘莘学子入不了学)。你心里终于对OO不满起来:为什么,为什么大家都说OO是救世主,但是却救不了我。答案是因为你将OO的设计原则遗忘在课本里了。开闭原则、优先使用组合,你还记得吗?
在我们很多OO程序员的脑子里总是存在这样一个观念:没有继承的程序不是OO的程序,看到重复总是想到继承。当初我也是这样想的,有的时候看到自己画的庞大的继承类图,心里在乐呵呵的笑。可继承总是不给面子,一个小小的变化就将这个看似稳定的体系弄的支离破碎。
还是回到我们的例子,在这个例子中变化的是各高校的报到步骤,本着发现变化、封装变化、隔离变化的原则我们将报到的步骤分离出来,独立成类。
这样我们就可以复用这些步骤了,有新的步骤实现只要添加更多的子类,并不需要修改原来的代码。(作业:在继续阅读之前根据上面的类图自己写出实现的代码来)
这就是所谓的策略设计模式:策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于算法的客户(DP)。
策略模式有三种参与者:
一、 Context 这个类保存了对策略的引用,并且调用实际的策略实现,有可能还提供一个接口,让策略可以访问它内部的数据,在这里就是我们的“高校”类。
二、 Strategy 策略接口,给算法族定义一个通用的接口,让客户以一种一致的方法去访问。(I教务处报到,I缴费)
三、 ConcreteStrategy 这就是具体的策略实现了,实现策略接口(各报到步骤的实现)。
如下图:
在我们的例子中报到的步骤就是算法族 比如“缴费”这个步骤,有多种缴费方式,我们将其封装起来,客户调用的时候并不需要了解你是怎么实现这个“缴费”的,这个过程对于客户来说是透明的。这些不同的“缴费”步骤之间是可以无缝的替换,而客户对此一点都不知觉。
好了,既然解决方案提出来了,我们就来实现它吧
首先我们定义所有的报到步骤的接口:
public interface I教务处报到
{
void 教务处报到();
}
public interface I缴费
{
void 缴费();
}
public interface I本院系报到
{
void 本院系报到();
}
public interface I教材科发教材
{
void 教材科发教材();
}
下面我实现两个教务处报到的步骤,其他的就当作课后作业了,呵呵。
public class 教务处报到A : I教务处报到
{
public void 教务处报到()
{
Console.Write("教务处报到,A类实现");
}
}
public class 教务处报到B : I教务处报到
{
public void 教务处报到()
{
MessageBox.Show("教务处报到,B类实现");
}
}
再看看我们的高校类的实现吧:
public class 高校
{
public 高校(I教务处报到 p教务处报到,I缴费 p缴费,I本院系报到 p本院系报到,I教材科发教材 p教材科发教材)
{
this._教务处报到 = p教务处报到;
this._缴费 = p缴费;
this._本院系报到 = p本院系报到;
this._教材科发教材 = p教材科发教材;
}
//为什么有了赋值的构造函数还要暴露这么多只写属性出来呢?
//这样就可以在运行时改变高校的报到步骤了,
//假如报到系统出现故障我们可以马上采取另外一种方案
//而不需要停止系统的运行
I教务处报到 _教务处报到;
public I教务处报到 教务处报到
{
set
{
_教务处报到 = value;
}
}
I缴费 _缴费;
public I缴费 缴费
{
set
{
_缴费 = value;
}
}
I本院系报到 _本院系报到;
public I本院系报到 本院系报到
{
set
{
_本院系报到 = value;
}
}
I教材科发教材 _教材科发教材;
public I教材科发教材 教材科发教材
{
set
{
_教材科发教材 = value;
}
}
//用上了策略模式,模板方法更加灵活了
//但现在还是不是模板方法了?
public void 报到()
{
教务处报到.教务处报到();
缴费.缴费();
本院系报到.本院系报到();
教材科发教材.教材科发教材();
}
}
Ok,我就把代码写这么多了,要这个代码运行起来还需要一些补充,这个高校类如何进行实例化才能更灵活也值得考虑。
看到没,利用组合我们也可以达到代码复用的目的,而且没有继承的弊端。
上面好像都是在说策略模式的好话,那策略模式有没有副作用呢?当然有
一、 虽说客户代码无须关心各个策略是如何实现的,但是它们还是要知道有多少种策略实现,该实现是干什么的,也就是客户代码需要知道策略的一些细节,这样才可以根据需要使用哪个策略,但是我们可以使用创建型模式来解决这个问题。
二、 有的时候策略需要从Context那里获取一些数据,这样造成双向的关联,而且有可能几个策略需要的数据都不一样,但是为了一致性不得不向它们传递相同的数据。
三、 也许大家会发现,使用策略模式后出现很多小类,实际上这也是所有设计模式的“通病”。
现实中的策略模式
大家对于PetShop这个应用肯定很熟悉,在PetShop 4.0里面就使用了策略设计模式:
在Petshop4的BLL项目中有一个OrderAsynchronous类和一个OrderSynchronous类,它们都继承自IorderStrategy。OrderSynchronous以一种同步的方式处理订单,而OrderAsynchronous先将订单放在队列里,然后再对队列里的订单进行处理,以一种异步方式。而在BLL中的Order类里通过反射从配置文件读取策略配置的信息以决定到底是使用哪种订单处理方式。这里就不贴代码了,有兴趣的可以去下载PetShop看看,主要关注这几个:PetShop.BLL.Order(如何使用策略以及如何根据配置文件实例化具体的策略)、PetShop.IBLLStrategy. IorderStrategy(策略的接口)、PetShop.BLL. OrderSynchronous、PetShop.BLL. OrderAsynchronous。
总结
在本篇我们从模板方法谈起,聊了一些模板方法随着项目的发展可能造成的问题,但这并不是模板方法的弊端,模板方法关注的是算法骨架的复用,如果你发觉新的问题出现,这可能就是模板方法不再适用的信号。通过我们对项目的扩展,发现继承在某些时候并不是都能达到代码复用的目的,这个时候我们应该考虑组合了,而且继承是一种静态的编译期的行为(针对像C#这种强类型静态语言而言),代码一经写定我们就没有选择的余地了。
前几天和别人在群里闲聊,谈到怎样学习设计模式,有人说设计模式靠悟,有人说设计模式靠经验的积累。悟也好,经验积累也好,我的感觉是不要把设计模式当作圣经,当一个人把一个事物当作圣经的时候总是很珍惜她,而且不会去亵渎她,这是学习模式的障碍。对于初学者来说应该有“熟读唐诗三百首,不会吟诗也会吟”的决心。