1.声明(H1字号)
设计模式中的设计思想、图片和部分代码参考自《Head First设计模式》,作者Eric Freeman & Elisabeth Freeman & Kathy Siezza & Bert Bates。
在这里我只是对这本书进行学习阅读,并向大家分享一些心得体会。
2.开篇语
大学毕业已经九个月了,但是从事工作正好一年。自从学编程起,自己还没有真正的开始进行设计模式的学习,以前总是在别人的框架中看到各种各样的设计模式,但是并不知道开发者为何这样设计,有时我心里总是在想,为什么别人要这么设计,为什么要把事情搞得这么麻烦呢?不光设计模式,就连Java中的接口,曾经也是让我非常不理解它的用途,但是随着学习的深入,逐渐发现大神们写的框架设计的非常之巧妙,而这里又涉及了大量的设计模式的知识,所以学习设计模式非常关键。程序员们总是自嘲码农,但是谁也不想永远当个码农对吧,所以在我心里,设计模式就是码农通向软件工程师中的重要一环。
我不觉得自己是个有毅力的人,因为曾经我计划过的很多事都未实现,现在开始学习设计模式,我希望自己能够坚持下去,这篇就是我真正开始学习设计模式的第一篇-策略设计模式。
由于本人才疏学浅,在阅读设计模式的时候有很多不懂的地方只能硬着头皮去理解,自己的想法是先在大脑中贯彻出这种思想,然后再在实际工作中去慢慢体会,所以可能会有很多理解不周甚至错误的地方,欢迎各位指出来,我会及时进行修改。
3.策略设计模式
策略设计模式简述:定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的客户,算法族的替换不会影响到客户端。
3.1需求(H2字号)
现在有一款游戏要设计,游戏中有各种各样的鸭子,现在我们开始采用OO(面向对象)技术进行设计。
3.2开始设计
既然是面向对象设计方式,那么不必多说,首先设计一个父类,然后设计两个子类,分别是野鸭和红头鸭。
//鸭子的父类
public abstract class Duck {
//所有的鸭子都会叫,所以这里由父类实现了叫的方法
public void quack() {
System.out.println("duck call");
}
//所有的鸭子都会游泳,所以这里由父类实现了游泳的方法
public void swim() {
System.out.println("duck swim");
}
//由于每个鸭子的外观不同,所以设计为抽象的由子类自行进行设计
public abstract void display();
}
//野鸭子
public class MallardDuck extends Duck{
@Override
public void display() {
System.out.println("野鸭子的外观");
}
}
//红头鸭子
public class RedheadDuck extends Duck{
@Override
public void display() {
System.out.println("红头鸭子的外观");
}
}
3.3功能添加
现在游戏主管决定对游戏添加新的功能,决定让鸭子飞起来,于此同时游戏中添加了一个新的鸭子角色:橡皮鸭(经常在浴室出现的),于是一位OO程序员Joe,就在父类Duck中添加了一个fly()方法,那么众多鸭子就可以具有飞的功能了。
//鸭子的父类
public abstract class Duck {
//所有的鸭子都会叫,所以这里由父类实现了叫的方法
public void quack() {
System.out.println("duck call");
}
//所有的鸭子都会游泳,所以这里由父类实现了游泳的方法
public void swim() {
System.out.println("duck swim");
}
//由于每个鸭子的外观不同,所以设计为抽象的由子类自行进行设计
public abstract void display();
public void fly() {
System.out.println("鸭子在飞");
}
}
//橡皮鸭
public class RubberDuck extends Duck{
//假的橡皮鸭子不会呱呱叫,只能发出吱吱的声音
@Override
public void quack() {
System.out.println("覆盖成吱吱叫");
}
@Override
public void display() {
System.out.println("外观是橡皮鸭");
}
}
但是这时公司的领导非常气愤,说在游戏中看见橡皮鸭子(假鸭子)飞是非常不合理的,于是Joe对橡皮鸭子进行如下更改。
//橡皮鸭
public class RubberDuck extends Duck{
//假的橡皮鸭子不会呱呱叫,只能发出吱吱的声音
@Override
public void quack() {
System.out.println("覆盖成吱吱叫");
}
@Override
public void display() {
System.out.println("外观是橡皮鸭");
}
@Override
public void fly() {
//覆盖改为什么也不做,为了实现不让橡皮鸭飞的目的
}
}
但是这样做并不能从根本解决问题,原因就是如果以后游戏中添加了新的鸭子:木头假鸭,那么这种鸭子既不能叫,也不能飞,难不成将quack()和fly()都重写一遍,而且还是仅仅都不做?这肯定是非常不合理的。
于是Joe又想到了新的办法,他把fly()和quack()方法从父类中抽取出来,然后设计成了两个接口,分别是Flyable接口和Quackable接口。
//飞行接口
public interface Flyable {
//飞行方法
public abstract void fly();
}
//叫接口
public interface Quackable {
//叫方法
public abstract void quack();
}
这样具有飞行行为和叫行为的鸭子采取实现对应的接口,这样看起来确实解决了强行覆盖父类方法的问题,但是这样却有个更致命的问题,因为我们知道接口中的方法没有方法体,如果有48个鸭子,它们的飞行行为大致形同,那么我们就需要在每个子类中都实现一个方法48遍,而且很有可能在改需求的时候,也有可能改48遍,这样的代码毫无复用性可言,那么这个问题到底该如何解决?
设计原则(H3字号)
找出应用中可能需要变化的部分,把他们独立出来,不要和那些不需要变化的代码混在一起。
换句话说,如果每次新的需求一来,都会使某方面的代码发生变化,那么这段代码就需要抽取出来,和其他稳定的代码有所区分。
3.4问题的解决思路
所以我们需要的是分开程序中变化的部分和不变的部分,其中变化的部分是fly()和quack(),那么我们则需要将这两个方法单独拿出来,之前将这两个方法设计成接口的思路是正确的,那么我们只需要提高接口的复用性就可以了。那么,我们可以设计如下两个接口,而fly()和quack()行为也从鸭子的类中单拿出来,将这两个行为设计为新类,并实现对应的两个接口。
那么这样鸭子的行为就单独设计成一个类,例如飞这个行为,就有两个实现类,分别是FlyWithWings和FlyNoWay。
那么这样设计,就可以让这两个行为被其他对象所复用,因为这两个行为已经与鸭子类无关了。
所以现在在设计鸭子类的时候,不需要在类的内部设计fly()和quack()两个方法,而是在Duck中保留飞行行为和叫行为的两个对象的引用,将飞行和叫委托给别人处理。
设计原则
针对接口编程,而不是针对实现编程。
这里的接口并非是java中狭义的接口(interface),而是广义上的接口,即:超类型。
3.5代码具体设计
//飞行行为的接口
public interface FlyBehavior {
//飞行方法
public abstract void fly();
}
//具有飞行行为的实现类
public class FlyWithWings implements FlyBehavior{
@Override
public void fly() {
System.out.println("实现鸭子飞");
}
}
//不具有飞行行为的实现类
public class FlyNoWay implements FlyBehavior{
@Override
public void fly() {
System.out.println("并不会飞");
}
}
//呱呱叫行为接口
public interface QuackBehavior {
//呱呱叫方法
public abstract void quack();
}
//具有呱呱叫的行为的实现类
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("实现鸭子呱呱叫");
}
}
//具有吱吱叫(橡皮鸭子的叫的方式)的行为的实现类
public class Squeak implements QuackBehavior {
public void quack() {
System.out.println("橡皮鸭子吱吱叫");
}
}
//不会叫的实现类
public class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("并不会叫");
}
}
//鸭子基类
public abstract class Duck {
//具有飞行行为的实现对象,鸭子飞行的委托者
FlyBehavior flyBehavior;
//具有叫行为的实现对象,鸭子叫的委托者
QuackBehavior quackBehavior;
public Duck() {
}
//各个鸭子的外貌不同具体由子类实现
abstract void display();
//鸭子的飞行行为委托给flyBehavior
public void performFly() {
flyBehavior.fly();
}
//鸭子的叫行为委托给quackBehavior
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.println("所有的鸭子都会游泳");
}
}
下面看看鸭子的某个具体实现类,例如野鸭的内部构造:
//野鸭
public class MallardDuck extends Duck {
//构造方法中制定了具体的飞行委托者和叫委托者
public MallardDuck() {
//注:quackBehavior和flyBehavior子类可以直接访问(只要和父类同包即可)
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
public void display() {
System.out.println("我长得像一个野鸭子");
}
}
下面书写测试案例,来展现一下野鸭子的各种行为:
//野鸭子的行为展示案例
public class MiniDuckSimulator {
public static void main(String[] args) {
//实例化野鸭子,MallardDuck在构造方法中对飞行和叫的委托者进行了初始化
Duck mallard = new MallardDuck();
//交给野鸭子的 叫委托者 展现叫行为
mallard.performQuack();
//交给野鸭子的 飞行委托者 展现飞行行为
mallard.performFly();
//使用的是父类的游泳方法
mallard.swim();
//使用的是自己本类中的外貌方法
mallard.display();
}
}
运行结果如下:
实现鸭子呱呱叫
实现鸭子飞
所有的鸭子都会游泳
我长得像一个野鸭子
3.6动态设定行为
因为在前文的设计原则中提到过,我们应该尽量的避免对具体实现进行编程,所以我们需要在Duck中提供对flyBehavior和quackBehavior做动态修改的方法,然后我们测试,动态设定行为的好处。
修改Duck类,新增setFlyBehavior和setQuackBehavior这两个方法:
//鸭子基类
public abstract class Duck {
//具有飞行行为的实现对象,鸭子飞行的委托者
FlyBehavior flyBehavior;
//具有叫行为的实现对象,鸭子叫的委托者
QuackBehavior quackBehavior;
public Duck() {
}
//设置具体的飞行委托者,可以在程序运行时,指定特定的飞行委托者
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
//设置具体的叫委托者,可以在程序运行时,指定特定的叫委托者
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
//各个鸭子的外貌不同具体由子类实现
abstract void display();
//鸭子的飞行行为委托给flyBehavior
public void performFly() {
flyBehavior.fly();
}
//鸭子的叫行为委托给quackBehavior
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.println("所有的鸭子都会游泳");
}
}
新增一个模型鸭类型:
//模型鸭子
public class ModelDuck extends Duck {
//初始化时
public ModelDuck() {
//飞行行为: 不会飞
flyBehavior = new FlyNoWay();
//叫行为: 呱呱叫
quackBehavior = new Quack();
}
public void display() {
System.out.println("我的外貌是一个模型鸭");
}
}
新增一个飞行行为,火箭带你飞:
//借助火箭飞行的飞行行为
public class FlyRocketPowered implements FlyBehavior {
public void fly() {
System.out.println("火箭带我飞");
}
}
下面我们修改一下测试类,看看这个动态行为的设定究竟有什么好处:
//野鸭子的行为展示案例
public class MiniDuckSimulator {
public static void main(String[] args) {
System.out.println("动态设定行为之前,我是一个这样的鸭子!!!");
//实例化模型鸭
Duck model = new ModelDuck();
//展现自己的飞行行为
model.performFly();
System.out.println();
System.out.println("动态设定行为之后,我逆袭成了这样的鸭子!!!");
//动态设置flyBehavior
//FlyRocketPowered覆盖掉了原ModelDuck构造器中的FlyNoWay
model.setFlyBehavior(new FlyRocketPowered());
//展现自己的飞行行为
model.performFly();
}
}
运行结果:
动态设定行为之前,我是一个这样的鸭子!!!
并不会飞
动态设定行为之后,我逆袭成了这样的鸭子!!!
火箭带我飞
经过了动态的行为设定,这个模型鸭成功的飞上了天。
3.7类图总览
现在我们站在高处俯瞰全局,则会更加的清晰明了:
我们可以把不同的行为看成不同的算法族,以Fly为例,我们可以根据具体需要来选择传递的FlyBehavior到底是FlyNoWay还是FlyWithWings,根据具体的需求选择具体的算法。
3.8多用组合
我们发现,Duck中既包含FlyBehavior又包含QuackBehavior,所以我们可以认为鸭子的行为是由这些行为模块组合而成的,相比于继承而言,组合的好处是:使系统具有很大的弹性,不仅可将算法族封装成类,更可以在运行时动态的改变行为,只要组合的对象符合正确的接口标准即可。
4.总结
策略模式思想总结:其思想是针对一组算法,将每一种算法都封装到具有共同接口的独立的类中,从而是它们可以相互替换。策略模式的最大特点是使得算法可以在不影响客户端的情况下发生变化,从而改变不同的功能。
策略模式的使用案例还可参考:http://baijiahao.baidu.com/s?id=1601547440739500969&wfr=spider&for=pc