策略模式
以经典的鸭子项目为例:
- 各种鸭子,比如北京鸭,野鸭,水鸭,玩具鸭等;
- 鸭子有各种行为,如游泳,飞行,叫等;
- 能够鸭子的行为信息。
1 用继承实现该需求
- 所有的鸭子都会呱呱叫也会游泳,所以由超类负责这部分的代码实现;
- 因为每种鸭子的外观不同,所以
display()
的方法时抽象的; - 每种鸭子负责实现自己的
display()
显示自己的外观;
此时,如果需要添加一个飞行的功能,要是在超类Duck
中添加加上fly()
方法,然后所有子类就会继承父类。这样看似没问题,但是不同的鸭子飞行的方式不同;MallardDuck可能飞的高,橡皮鸭不会飞,这样每个子类就要重写fly()
方法;每新增方法就要根据特性修改子类代码实现。
缺点:
- 代码在多个子类中重复;
- 很难知道所有鸭子的全部行为;
- 运行时的行为不容易改变;
- 改变会牵一发而动全身,造成其他鸭子不想要的改变。
2 利用接口实现
显然使用继承的方式并不理想,每当有新的鸭子类出现或者有新的行为出现,就要重写所有的方法或者修改所有的子类。可以把fly()
方法抽取出来,放入一个Flyable
接口中。这样,只有会飞的鸭子才实现此接口。同样可以设计一个Quackable
接口,因为不是所有鸭子都会叫。(将变化的部分抽取出来,与不变的保持距离)
这样写之后,每个实现接口的类都要实现接口的fly()
或者quack()
方法,会造成代码无法复用;无论何时要修改某个行为,必须往下追踪并在每一个定义此行为的类中修改它。
3 分开变化和不会变的部分
开发上不管软件当初设计的多么完美,一段时间之后,总要作出改变;否则这个软件就死亡了。
把会变化的部分抽取出并封装起来,不要和那些不需要变化的代码放在一起这是一个重要的设计原则;换句话说如果每次需求出来,都会使某方面的代码发生变化,那么就可以确定,这部分代码需要抽取出来,和其他稳定的代码有所区分。这是每个设计模式都背后精神所在。
将鸭子的行将放在单独的类中,该类专门提供类某种行为的接口实现。
面向接口编程,不要针对实现编程。 利用接口代表某种行为,比如:FlyBehavior
和QuackBehavior
,让鸭子使用这些行为接口。而某个具体的行为去实现这些接口,而不是让鸭子本身去实现这些行为。
“面向接口编程”中的“接口”关键是多态,利用多态程序要针对超类编程,执行时会根据实际情况执行实际的行为;不会被绑死咋超类的实现上。更明确的可以说成:变量的声明要用超类型。
这样的设计,可以让飞行和叫的行为被其他对象服用,因为这些行为已经和具体的鸭子无关了;
当增加一些新的行为,不会影响到现有的行为类,也不会影响到使用到飞行行为的鸭子类。
3.1 代码实现
- 以飞行行为为例,创建飞行接口,已经实现类
/**
* @author user
* @date 2020/6/11 16:35
*/
public interface FlyBehavior {
void fly();
}
/**
* @author user
* @date 2020/6/11 16:44
*/
public class FlyFast implements FlyBehavior {
public void fly() {
System.out.printf("快速飞行");
}
}
/**
* @author user
* @date 2020/6/11 16:45
*/
public class FlyNoway implements FlyBehavior {
public void fly() {
System.out.printf("不会飞");
}
}
- 首先在
Duck
类中加入flyBehavior
,quackBehavior
两个变量和performQuack()
,performFly()
两个方法:
package com.gu.strategy.duck03;
/**
* @author user
* @date 2020/6/11 16:31
*/
public abstract class Duck {
protected FlyBehavior flyBehavior;
protected QuackBehavior quackBehavior;
/**
* 鸭子不是自己去实现叫的行为,而是托管给quackBehavior引用的对象
*/
public void performQuack(){
quackBehavior.quack();
}
/**
* 鸭子不是自己去实现飞行行为,而是托管给flyBehavior引用的对象
*/
public void performFly(){
flyBehavior.fly();
}
public void swim(){
System.out.println("游泳");
}
public abstract void display();
- 给具体的鸭子添加行为;各种鸭子可以根据flyBehavior 的实际引用,表现出不同的飞行方法
package com.gu.strategy.duck03;
/**
1. @author user
2. @date 2020/6/11 17:34
*/
public class MallardDuck extends Duck{
/**
* MallardDuck会快速的飞行也会嘎嘎叫,当performFly()被调用时就会托管给FlyFast的fly()方法
*/
public MallardDuck() {
this.flyBehavior = new FlyFast();
this.quackBehavior = new QuackGaga();
}
public void display() {
System.out.println("是一只MallardDuck");
}
}
- 测试
这里也可以动态的改变鸭子的行为,只需要给
flyBehavior
和quackBehavior
添加set()
方法,就可以实现动态的改变行为。
整体的结构:来自《head first 设计模式》书中的截图:
这里也体现了一个设计原则:**多用组合,少用继承。**即:Has-A
可能比Is-A
更好。
4 策略模式定义
定义了算法族,分别封装起来,让他们直接可以相互替换,此模式让算法的变化独立于使用算法的客户。
5 在JDK中的使用
5.1 比较器Comparator
在Java的集合框架中,经常需要通过构造方法传入一个比较器Comparator,或者创建比较器传入Collections的静态方法中作为方法参数,进行比较排序等,使用的是策略模式。
在该比较架构中,Comparator就是一个抽象的策略;一个类实现该结构,并实现里面的compare方法,该类成为具体策略类;Collections类就是环境角色,他将集合的比较封装成静态方法对外提供api。
5.2 ThreadPoolExecutor中的四种拒绝策略
在创建线程池时,需要传入拒绝策略,当创建新线程使当前运行的线程数超过maximumPoolSize时,将会使用传入的拒绝策略进行处理。
AbortPolicy:直接抛出异常。
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。