设计模式解读之一: 策略模式
1. 模式定义
把会变化的内容取出并封装起来,以便以后可以轻易地改动或扩充部分,而不影响不需要变化的其他部分;
2. 问题缘起
当涉及至代码维护时,为了复用目的而使用继承,结局并不完美。对父类的修改,会影响到子类型。在超类中增加的方法,会导致子类型有该方法,甚至连那些不该具备该方法的子类型也无法免除。示例,一个鸭子类型:
public abstract class Duck {
//所有的鸭子均会叫以及游泳,所以父类中处理这部分代码
public void quack() {
System.out.println("Quack");
}
public void swim() {
System.out.println("All ducks float, even decoys.");
}
//因为每种鸭子的外观是不同的,所以父类中该方法是抽象的,由子类型自己完成。
public abstract void display();
}
public class MallardDuck extends Duck {
//野鸭外观显示为绿头
public void display() {
System.out.println("Green head.");
}
}
public class RedHeadDuck extends Duck {
//红头鸭显示为红头
public void display() {
System.out.println("Red head.");
}
}
public class RubberDuck extends Duck {
//橡皮鸭叫声为吱吱叫,所以重写父类以改写行为
public void quack() {
System.out.println("Squeak");
}
//橡皮鸭显示为黄头
public void display() {
System.out.println("Yellow head.");
}
}
上述代码,初始实现得非常好。现在我们如果给Duck.java中加入fly()方法的话,那么在子类型中均有了该方法,于是我们看到了 会飞的橡皮鸭子,你看过吗?当然,我们可以在子类中通过空实现重写该方法以解决该方法对于子类型的影响。但是父类中再增加其它的方法呢?
通过继承在父类中提供行为,会导致以下缺点:
a. 代码在多个子类中重复;
b. 运行时的行为不容易改变;
c. 改变会牵一发动全身,造成部分子类型不想要的改变;
好啦,还是刚才鸭子的例子,你也许想到使用接口,将飞的行为、叫的行为定义为接口,然后让Duck的各种子类型实现这些接口。这时侯代码类似于:
public abstract class Duck {
//将变化的行为 fly() 以及quake()从Duck类中分离出去定义形成接口,有需求的子类中自行去实现
public void swim() {
System.out.println("All ducks float, even decoys.");
}
public abstract void display();
}
//变化的 fly() 行为定义形成的接口
public interface FlyBehavior {
void fly();
}
//变化的 quack() 行为定义形成的接口
public interface QuackBehavior {
void quack();
}
//野鸭子会飞以及叫,所以实现接口 FlyBehavior, QuackBehavior
public class MallardDuck extends Duck implements FlyBehavior, QuackBehavior{
public void display() {
System.out.println("Green head.");
}
public void fly() {
System.out.println("Fly.");
}
public void quack() {
System.out.println("Quack.");
}
}
//红头鸭子会飞以及叫,所以也实现接口 FlyBehavior, QuackBehavior
public class RedHeadDuck extends Duck implements FlyBehavior, QuackBehavior{
public void display() {
System.out.println("Red head.");
}
public void fly() {
System.out.println("Fly.");
}
public void quack() {
System.out.println("Quack.");
}
}
//橡皮鸭不会飞,但会吱吱叫,所以只实现接口QuackBehavior
public class RubberDuck extends Duck implements QuackBehavior{
//橡皮鸭叫声为吱吱叫
public void quack() {
System.out.println("Squeak");
}
//橡皮鸭显示为黄头
public void display() {
System.out.println("Yellow head.");
}
}
上述代码虽然解决了一部分问题,让子类型可以有选择地提供一些行为(例如 fly() 方法将不会出现在橡皮鸭中).但我们也看到,野鸭子MallardDuck.java和红头鸭子RedHeadDuck.java的一些相同行为代码不能得到重复使用。很大程度上这是从一个火坑跳到另一个火坑。
在一段程序之后,让我们从细节中跳出来,关注一些共性问题。不管使用什么语言,构建什么应用,在软件开发上,一直伴随着的不变的真理是:需要一直在变化。不管当初软件设计得多好,一段时间之后,总是需要成长与改变,否则软件就会死亡。
我们知道,继承在某种程度上可以实现代码重用,但是父类(例如鸭子类Duck)的行为在子类型中是不断变化的,让所有子类型都有这些行为是不恰当的。我们可以将这些行为定义为接口,让Duck的各种子类型去实现,但接口不具有实现代码,所以实现接口无法达到代码复用。这意味着,当我们需要修改某个行为,必须往下追踪并在每一个定义此行为的类中修改它,一不小心,会造成新的错误。
设计原则:把应用中变化的地方独立出来,不要和那些不需要变化的代码混在一起。这样代码变化引起的不经意后果变少,系统变得更有弹性。
按照上述设计原则,我们重新审视之前的Duck代码。
1) 分开变化的内容和不变的内容
Duck类中的行为 fly(), quack(), 每个子类型可能有自己特有的表现,这就是所谓的变化的内容。
Duck类中的行为 swim() 每个子类型的表现均相同,这就是所谓不变的内容。
我们将变化的内容从Duck()类中剥离出来单独定义形成接口以及一系列的实现类型。将变化的内容定义形成接口可实现变化内容和不变内容的剥离。其实现类型可实现变化内容的重用。这些实现类并非Duck.java的子类型,而是专门的一组实现类,称之为"行为类"。由行为类而不是Duck.java的子类型来实现接口。这样,才能保证变化的行为独立于不变的内容。于是我们有:
变化的内容:
//变化的 fly() 行为定义形成的接口
public interface FlyBehavior {
void fly();
}
//变化的 fly() 行为的实现类之一
public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("I'm flying.");
}
}
//变化的 fly() 行为的实现类之二
public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("I can't fly.");
}
}
-----------------------------------------------------------------
//变化的 quack() 行为定义形成的接口
public interface QuackBehavior {
void quack();
}
//变化的 quack() 行为实现类之一
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("Quack");
}
}
//变化的 quack() 行为实现类之二
public class Squeak implements QuackBehavior {
public void quack() {
System.out.println("Squeak.");
}
}
//变化的 quack() 行为实现类之三
public class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("<< Slience >>");
}
}
通过以上设计,fly()行为以及quack()行为已经和Duck.java没有什么关系,可以充分得到复用。而且我们很容易增加新的行为, 既不影响现有的行为,也不影响Duck.java。但是,大家可能有个疑问,就是在面向对象中行为不是体现为方法吗?为什么现在被定义形成类(例如Squeak.java)?在OO中,类代表的"东西"一般是既有状态(实例变量)又有方法。只是在本例中碰巧"东西"是个行为。既使是行为,也有属性及方法,例如飞行行为,也需要一些属性记录飞行的状态,如飞行高度、速度等。
2) 整合变化的内容和不变的内容
Duck.java将 fly()以及quack()的行为委托给行为类处理。
不变的内容:
public abstract class Duck {
//将行为类声明为接口类型,降低对行为实现类型的依赖
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public void performFly() {
//不自行处理fly()行为,而是委拖给引用flyBehavior所指向的行为对象
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.println("All ducks float, even decoys.");
}
public abstract void display();
}
Duck.java不关心如何进行 fly()以及quack(), 这些细节交由具体的行为类完成。
public class MallardDuck extends Duck{
public MallardDuck() {
flyBehavior=new FlyWithWings();
quackBehavior=new Quack();
}
public void display() {
System.out.println("Green head.");
}
}
测试类:
public class DuckTest {
public static void main(String[] args) {
Duck duck=new MallardDuck();
duck.performFly();
duck.performQuack();
}
}
在Duck.java子类型MallardDuck.java的构造方法中,直接实例化行为类型,在编译的时侯便指定具体行为类型。当然,我们可以:
1) 我们可以通过工厂模式或其它模式进一步解藕(可参考后续模式讲解);
2) 或做到在运行时动态地改变行为。
3) 动态设定行为
在父类Duck.java中增加设定行为类型的setter方法,接受行为类型对象的参数传入。为了降藕,行为参数被声明为接口类型。这样,既便在运行时,也可以通过调用这二个方法以改变行为。
public abstract class Duck {
//在刚才Duck.java中加入以下二个方法。
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior=flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior=quackBehavior;
}
//其它方法同,省略...
}
测试类:
public class DuckTest {
public static void main(String[] args) {
Duck duck=new MallardDuck();
duck.performFly();
duck.performQuack();
duck.setFlyBehavior(new FlyNoWay());
duck.performFly();
}
}
如果,我们要加上火箭助力的飞行行为,只需再新建FlyBehavior.java接口的实现类型。而子类型可通过调用setQuackBehavior(...)方法动态改变。至此,在Duck.java增加新的行为给我们代码所带来的困绕已不复存在。
该是总结的时侯了,让我们从代码的水中浮出来,做一只在水面上自由游动的鸭子吧:
3. 解决方案
MallardDuck 继承 Duck抽象类; -> 不变的内容
FlyWithWings 实现 FlyBehavior接口; -> 变化的内容,行为或算法
在Duck.java提供setter方法以装配关系; -> 动态设定行为
以上就是策略模式的实现三步曲。接下来,让我们透过步骤看本质:
1) 初始,我们通过继承实现行为的重用,导致了代码的维护问题。 -> 继承, is a
2) 接着,我们将行为剥离成单独的类型并声明为不变内容的实例变量并通过 -> 组合, has a
setter方法以装配关系;
继承,可以实现静态代码的复用;组合,可以实现代码的弹性维护;使用组合代替继承,可以使代码更好地适应软件开发完后的需求变化。
策略模式的本质:少用继承,多用组合
分享到:
相关推荐
设计模式解读之一: 策略模式——鸭子游戏.doc
策略模式的深刻讲解,大家可以看看,我觉得不错1
1.7 Java5 Concurrent包中的锁机制 1.8 设计模式解读之一: 策略模式 1.9 外观模式(Facade Pattern) 追MM1.10 做一个通讯项目的一点心得 1.11 Hibernate源码研究碎得整理(一到十三)1.12 解决万恶的大数问题 1.13 ...
阿里10年工作经验大牛讲解设计模式(带案例) 设计模式综合案例 jdk中的设计模式 ...策略模式 访问者模式 解释器模式 命令模式 调停者模式 状态模式 多人联机射击游戏在实际开发中综合使用设计模式
用VS2010实现的收费系统,包含简单工厂模式
本书讲述了以下五个部分的内容:商业模式画布,一种描述商业模式,分析商业模式和设计模式的工具;基于领先商业思想家的商业模式式样;帮助你设计商业模式的技术;通过商业模式视角重新解读策略;一个帮你设计创新...
Android源码设计模式解析与实战读书笔记源代码 说明: 包名factorypattern.normal表示的是工厂方法模式的普通用法 包名factorypattern.practices表示的是工厂方法模式的常用 包名observerpattern表示的是观察者模式...
考虑电动汽车充放电地点的分散性和时间的随机性,提出了一种区域内电动汽车充放电控制策略。通过仿真计算,得到了该控制方式下区域内电动汽车充放电对负荷曲线的影响。电动汽车充电负荷作为可调度负荷,可减小负荷...
第72讲:Netty线程模型深度解读与架构设计原则 第73讲:Netty底层架构系统总结与应用实践 第74讲:Netty对于异步读写操作的架构思想与观察者模式的重要应用 第75讲:适配器模式与模板方法模式在入站处理器中的...
目录、区位分析、交通分析、周边现状、文脉资源、景观分析、上位规划、用地规划、规划条件解读、强排汇总、规划方案、全高层模式、洋房最大化、高低配模式、总平面图、鸟瞰图、五重归家流线、强排比选 、商业分析、...
【目的】 如果您希望弄懂框架的原理,但是苦于不知道如何读框架源码?...3.策略模式:【45:40】 第二课:mybatis-generator源码分析-生成文件 这节课分为如下几段: 1. 【00:00-10:20】:上节课内容回顾 2.
图纸包含:规划解读、总体纲要、基地大环境、上位规划解读、设计分析、规划目标、总图推演、总平面图、.交通分析图、消防分析图、景观结构分析图、商业产品设计、住宅产品设计、小区建筑节点、产品设计、停车设计、...
图纸包含:目录、前期分析、规划策略、规划方案、产品设计、造型设计、专项设计、用地规划、规划条件解读、强排汇总、规划方案、全高层模式、洋房最大化、高低配模式、总平面图、鸟瞰图、五重归家流线、强排比选 、...
文字识别解读小程序的设计与实现 小程序 MINA JavaScript 文字识别 基于小程序开发设计一款智能文字识别查询小程序,采用Java音进行开发,使用开放API现图片文字识别,不用安装、使用方便,对方便人民的生活产生...
图纸包含:目录、区位分析、用地概况、目录、规划条件解读、强排汇总、规划方案、全高层模式、洋房最大化、高低配模式、总平面图、鸟瞰图、五重归家流线、强排比选 、商业分析、产品设计、周边竞品、产品策略、产品...
这个专栏为你设计了... 除了以上三大类技术点之外,构建一个微服务系统必不可少的架构技能,包含对外 SDK 设计、部署策略、服务解耦和分布式事务、立体化监控,这一模块将帮助你构建一个更加完整的架构能力体系。
3.2 客户终端、流量业务偏好的快速识别 3.3 客户兴趣识别技巧:一看二问三验证 情景演练与讨论:基于客户的需求探寻话术演练 4,主动积极引导 4.1 客户为什么喜欢去老地方? 4.2 主动引导的时机把握 4.3 引导话术...
Web MVC 框架、网站性能优化与安全策略、设计模式与架构、网站设计与网页配色、Java Web典型项目开发案例等。配书光盘附带了实例的源程序。 《Java Web开发实例大全(提高卷)》既适合Java Web程序员参考和查阅,也...
Channel选择器工厂与轮询算法及注册底层实现 72_Netty线程模型深度解读与架构设计原则 73_Netty底层架构系统总结与应用实践 74_Netty对于异步读写操作的架构思想与观察者模式的重要应用 75_适配器模式与模板方法模式...