我们通过一个鸭子游戏的案例来逐步理解策略模式。这个游戏的需求很简单,大家可以想象一下这样一个画面,
打开游戏界面,左边界面大部分区域是用来显示鸭子的,这些鸭子会在界面上游来游去,同时发出叫声,右面的小部分界面列出了当前游戏版本支持的鸭子类型,选择鸭子类型后就可以创建对应类型的鸭子出来,在左边的界面上显示出来。
鸭子游戏V1版本实现,基于上面的需求,我们应用面向对象的知识,抽象出鸭子基类,它有游泳和叫两个行为,同时为了让每种鸭子在游戏界面上有不同的外观,在加上一个抽象的外观方法。
抽象的鸭子基类代码:
/**
* 鸭子抽象类
* 所有的鸭子都应该继承至该类
*/
public abstract class DuckV1 {
/**
* 抽象的游泳方法
* 所有的鸭子都是在水里面游泳的。
*/
public void swim(){
System.out.println("在水里游泳");
}
/**
* 鸭子叫
* 所有的鸭子都是"呱呱呱"的叫
*/
public void quack(){
System.out.println("呱呱呱");
}
/**
* 鸭子外观
* 不同的鸭子有不同的外观
*/
public abstract void display();
}
接下类,我们来实现两个具体的鸭子,“野鸭"和"红头鸭”。
/**
* 野鸭
*/
public class MallardDuckV1 extends DuckV1 {
@Override
public void display() {
System.out.println("野鸭的外观是绿头");
}
}
/**
* 红头鸭
*/
public class RedheadDuckV1 extends DuckV1{
@Override
public void display() {
System.out.println("红头鸭的头是红色的");
}
}
现在,我们来创建具体的鸭子,让它们玩起来吧。
/**
* V1版鸭子游戏的测试
*/
public class DuckV1Main {
public static void main(String[] args) {
//一只野鸭在玩耍
DuckV1 duck1 = new MallardDuckV1();
duck1.display();
duck1.swim();
duck1.quack();
System.out.println();
//一只红头鸭在玩耍
DuckV1 duck2 = new RedheadDuckV1();
duck2.display();
duck2.swim();
duck2.quack();
System.out.println();
}
}
嗯,目前看起来还不错。
现在,市场反馈说,鸭子类型太少,新加入一种"橡皮鸭"进来。
按照目前的设计,新加入的橡皮鸭是一种具体的鸭子,只要继承我们的抽象鸭子就好了。
我们来实现一版看看:
/**
* 橡皮鸭
*/
public class RubberDuckV1 extends DuckV1 {
@Override
public void display() {
System.out.println("橡皮鸭好像都是黄色的");
}
}
然后,创建一只橡皮鸭,它也加入玩耍的鸭子中去。
//一只橡皮鸭
DuckV1 duck3 = new RubberDuckV1();
duck3.display();
duck3.fly();
duck3.swim();
duck3.quack();
橡皮鸭玩耍时看起来像是这样的:
橡皮鸭好像都是黄色的
在水里游泳
呱呱呱
咦,感觉好像哪里不大对劲,橡皮鸭貌似是"吱吱吱"的叫吧,不是"呱呱呱"的叫,嗯,对,这是一个BUG。
好吧,我们让橡皮鸭重写鸭子基类的叫的方法,覆盖为"吱吱吱"
@Override
public void quack() {
System.out.println("吱吱吱");
}
这样,看起来问题解决了,新加入的橡皮鸭也可以愉快的玩耍了。
一段时间过后,迫于市场竞争的压力,鸭子游戏需要加入新的要素来提高市场占有率。老板召集一群策划大佬头脑风暴会议后,决定"让鸭子飞起来",以此来提高游戏的体验。
嗯,对于这个需求,由于我们是面向对象设计,看起来只需要在抽象基类中加上飞行方法就行了。
说干就干,先试试看,在鸭子基类中:
/**
* 新添加的飞的方法
*/
public void fly(){
System.out.println("鸭子飞吧...");
}
接下来,我们测试下游戏:
public static void main(String[] args) {
//一只野鸭在玩耍
DuckV1 duck1 = new MallardDuckV1();
duck1.display();
duck1.fly();
duck1.swim();
duck1.quack();
System.out.println();
//一只红头鸭在玩耍
DuckV1 duck2 = new RedheadDuckV1();
duck2.display();
duck2.fly();
duck2.swim();
duck2.quack();
System.out.println();
//一只橡皮鸭
DuckV1 duck3 = new RubberDuckV1();
duck3.display();
duck3.fly();
duck3.swim();
duck3.quack();
}
重点关注橡皮鸭的玩耍姿势:
橡皮鸭好像都是黄色的
鸭子飞吧...
在水里游泳
吱吱吱
什么情况?橡皮鸭在界面上飞来飞去,飞得还很high。如果老板没有觉得这个"创意"很新颖的话,怕是要被骂得很惨。
为了让橡皮鸭不飞起来,好像只需要覆盖基类的飞行方法就行了吧,这和之前处理"叫"的方法是一样的。(我果然很厉害)
@Override
public void fly() {
//橡皮鸭不会飞
}
搞定,收工!
好了,我们来分析下,V1版本的核心设计是通过继承,来达到复用的目的。不过对于我们的鸭子游戏,大家思考一分钟,暴露出那些问题?(tips:发生变化)
如果鸭子基类中加入新的行为(跳舞),那么所有的鸭子都自动继承这些行为,即在基类中的变化会影响到所有子类,当子类发现这种行为不合适时(比如橡皮鸭不会跳舞),就需要覆盖基类的方法。如果鸭子类型变多越来越多,这个维护难度也会越来越大。
同理,新加入鸭子类型时,也需要被迫的去检查需要覆盖哪些方法,真是头大。
嗯,我们需要来修改下我们的设计,那么用接口如何?
我们基类中的所有行为抽取出来,形成单独的接口,比如Quackable、Flyable。此时,有对应行为的鸭子就去实现对应的接口,没有相应行为的鸭子就不实现。
好,我们也试试实现这种设计,看看效果。
鸭子基类:
/**
* 鸭子抽象类
* 所有的鸭子都应该继承至该类
*/
public abstract class DuckV2 {
/**
* 抽象的游泳方法
* 所有的鸭子都是在水里面游泳的。
*/
public void swim(){
System.out.println("在水里游泳");
}
/**
* 鸭子外观
* 不同的鸭子有不同的外观
*/
public abstract void display();
}
“叫”接口:
/**
* 叫接口
*/
public interface Quackable {
void quack();
}
“飞”接口:
/**
* 飞行接口
*/
public interface Flyable {
void fly();
}
野鸭子实现,能叫也能飞:
/**
* 野鸭
*/
public class MallardDuckV2 extends DuckV2 implements Flyable,Quackable {
@Override
public void display() {
System.out.println("野鸭的外观是绿头");
}
@Override
public void fly() {
System.out.println("野鸭用翅膀在天上飞");
}
@Override
public void quack() {
System.out.println("呱呱呱");
}
}
红头鸭子的实现,它也能叫和飞:
/**
* 红头鸭
*/
public class RedheadDuckV2 extends DuckV2 implements Flyable,Quackable {
@Override
public void display() {
System.out.println("红头鸭的头是红色的");
}
@Override
public void fly() {
System.out.println("野鸭用翅膀在天上飞");
}
@Override
public void quack() {
System.out.println("呱呱呱");
}
}
最后,橡皮鸭的实现,会叫不会飞:
/**
* 橡皮鸭
*/
public class RubberDuckV2 extends DuckV2 implements Quackable {
@Override
public void display() {
System.out.println("橡皮鸭好像都是黄色的");
}
@Override
public void quack() {
System.out.println("橡皮鸭是吱吱吱的叫");
}
}
OK,鸭子们,Let’s Play!
/**
* V2版鸭子游戏的测试
*/
public class DuckV2Main {
public static void main(String[] args) {
//一只野鸭在玩耍
DuckV2 duck1 = new MallardDuckV2();
duck1.display();
duck1.swim();
((Flyable)duck1).fly();
((Quackable)duck1).quack();
System.out.println();
//一只红头鸭在玩耍
DuckV2 duck2 = new RedheadDuckV2();
duck2.display();
duck2.swim();
((Flyable)duck2).fly();
((Quackable)duck2).quack();
System.out.println();
//一只橡皮鸭
DuckV2 duck3 = new RubberDuckV2();
duck3.display();
duck3.swim();
((Quackable)duck3).quack();
}
}
满足需求,玩得欢实。
观察下V2版的UML图:
V2版,通过将鸭子中的最有可能变化的行为单独抽取出来形成接口,已达到具体鸭子可以自由组合实现行为接口,来动态组合鸭子的行为。
但是我们非常容易发现V2的缺点,大量的代码重复(比如野鸭和红头鸭都是用翅膀飞,他们的飞的方法实现是重复的),客户端使用困难,需要知道具体鸭子实现了哪些行为接口,然后强制转为行为接口类型,才能调用对应行为的方法。
针对V2的缺点,我们能不能这样处理,将行为接口的具体行为单独封装起来,比如飞行行为的具体实现包括,用翅膀飞,不能飞两种,那么提供这两种实现出来,具体鸭子通过组合的方式实现复用,这样就消除了V2版本中代码重复的问题。另外,在鸭子基类中定义鸭子的行为方法,但是实现通过组合鸭子的各种行为接口,调用行为接口的方法来实现行为,这样客户端看到的鸭子就是统一的类型,解决类型转换的问题。
说干就干,看看V3版本的实现:
先定义行为接口(和V2版本的接口一样),不过为了区别,这儿换个名字:
/**
* 叫行为
*/
public interface QuackBehavior {
void quack();
}
/**
* 飞行行为
*/
public interface FlyBehavior {
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 class Quack implements QuackBehavior {
@Override
public void quack() {
System.out.println("呱呱呱的叫");
}
}
public class Squeak implements QuackBehavior {
@Override
public void quack() {
System.out.println("吱吱吱的叫");
}
}
鸭子基类,组合鸭子行为
/**
* 鸭子抽象类
* 所有的鸭子都应该继承至该类
*/
public abstract class DuckV3 {
//飞行行为
FlyBehavior flyBehavior;
//叫的行为
QuackBehavior quackBehavior;
/**
* 抽象的游泳方法
* 所有的鸭子都是在水里面游泳的。
*/
public void swim(){
System.out.println("在水里游泳");
}
public void fly(){
flyBehavior.fly();
}
//鸭子的叫委托给行为抽象了。
public void quack(){
quackBehavior.quack();
}
/**
* 鸭子外观
* 不同的鸭子有不同的外观
*/
public abstract void display();
}
接下来,实现具体的鸭子了。
野鸭,它通过翅膀飞,呱呱呱的叫。
/**
* 野鸭
*/
public class MallardDuckV3 extends DuckV3 {
public MallardDuckV3() {
//定义野鸭的行为
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
@Override
public void display() {
System.out.println("野鸭的外观是绿头");
}
}
红头鸭,用翅膀飞,呱呱呱的叫
/**
* 红头鸭
*/
public class RedheadDuckV3 extends DuckV3{
public RedheadDuckV3() {
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
@Override
public void display() {
System.out.println("红头鸭的头是红色的");
}
}
橡皮鸭,不会飞,吱吱吱的叫
/**
* 橡皮鸭
*/
public class RubberDuckV3 extends DuckV3{
public RubberDuckV3() {
flyBehavior = new FlyNoWay();
quackBehavior = new Squeak();
}
@Override
public void display() {
System.out.println("橡皮鸭好像都是黄色的");
}
}
按照惯例,先让鸭子们玩一会儿。
/**
* V3版鸭子游戏的测试
*/
public class DuckV3Main {
public static void main(String[] args) {
//一只野鸭在玩耍
DuckV3 duck1 = new MallardDuckV3();
duck1.display();
duck1.fly();
duck1.quack();
System.out.println();
//一只红头鸭在玩耍
DuckV3 duck2 = new RedheadDuckV3();
duck2.display();
duck2.quack();
duck2.fly();
System.out.println();
//一只橡皮鸭
DuckV3 duck3 = new RubberDuckV3();
duck3.display();
duck3.fly();
duck3.quack();
System.out.println();
}
}
唉哟,不错哦。貌似鸭子们玩的都不错哦。(老板是不是加工资了……)
同样,观察下V3版本的UML图:
V3版的实现解决V1和V2中的缺点,那么这个设计在应对新需求时表现怎么样?假设某个产品脑洞大开,在鸭子游戏中加入一种模型鸭的类型,这种鸭子不会叫,但是我们可以给它装上火箭推进器,让它飞起来。(老板说,这个点子不错,开发出来上线看看效果,开发心里。。。。)
还好,V3版的设计中,我们不会修改原有的代码,只需要扩展类就行了。
首先,我们要扩展我们行为实现,不会叫和火箭飞。
public class MuteQuack implements QuackBehavior {
@Override
public void quack() {
//不会叫
}
}
/**
* 火箭助力
*/
public class FlyRocketPower implements FlyBehavior {
@Override
public void fly() {
System.out.println("火箭助力飞行...");
}
}
然后,实现模型鸭,组合这两种新的具体行为:
/**
* 新增一个模型鸭
*/
public class ModelDuckV3 extends DuckV3{
public ModelDuckV3() {
//模型鸭不会飞,但装了火箭推进器
flyBehavior = new FlyRocketPower();
quackBehavior = new MuteQuack();
}
@Override
public void display() {
System.out.println("我是一只模型鸭");
}
}
现在,让这只模型鸭加入进来看看:(DockV3Main中加入)
//一只模型鸭
DuckV3 duck4 = new ModelDuckV3();
duck4.display();
duck4.fly();
duck4.quack();
输出
我是一只模型鸭
火箭助力飞行...
很好,这样我们就只通过扩展类的方式,来满足了新的需求(再奇葩也能实现),很好的满足了设计模式中的开闭原则。
到这里,我们就已经通过一个案例将我们今天要了解的策略模式引出了,下面我们看下正式的定义。
策略模式定义了一组算法族,并分别封装起来,它们之间可以互相替换。策略模式让算法独立于使用算法的客户端,算法可以独立演进。
用代码说明一下.
抽象的策略(可以是接口、也可以是抽象类)
/**
* 策略抽象(算法抽象)
*/
public interface Strategy {
void calculate();
}
具体的策略算法实现
/**
* 具体策略A
*/
public class ConcreteStrategyA implements Strategy {
@Override
public void calculate() {
System.out.println("策略A的算法");
}
}
/**
* 具体策略B
*/
public class ConcreteStrategyB implements Strategy {
@Override
public void calculate() {
System.out.println("策略B的算法");
}
}
将策略组合进去的上下文
/**
* 策略上下文,用来组合抽象策略
*/
public class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void calculate(){
strategy.calculate();
}
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
}
使用策略的客户
/**
* 客户
*/
public class StrategyMain {
public static void main(String[] args) {
Context context = new Context(new ConcreteStrategyA());
context.calculate();
context.setStrategy(new ConcreteStrategyB());
context.calculate();
}
}
从鸭子游戏的设计过程中,我们可以总结一些面向对象的设计技巧:
为了加深理解,在来一个排序的例子。(这个例子URL是https://java2blog.com/strategy-design-pattern-java/)
原始需求,现有一个List,需要对其进行排序,且排序算法是可以指定的。
假设,当前需要支持的排序算法只有两类,设计V1版本实现如下:
/**
* 排序类型
*/
public enum SortingTypeV1 {
MERGE_SORT, QUICK_SORT;
}
/**
* 排序管理器
*/
public class SortingManagerV1 {
List list;
public SortingManagerV1(List list) {
this.list = list;
}
public void sortListBaseOnType(SortingTypeV1 type){
System.out.println("===================================");
System.out.println("Sorting List based on Type");
System.out.println("===================================");
if(type == SortingTypeV1.MERGE_SORT){
sortListUsingMergeSort();
}else if(type == SortingTypeV1.QUICK_SORT){
sortListUsingQuickSort();
}else {
throw new RuntimeException("not support type");
}
}
private void sortListUsingQuickSort() {
System.out.println("Sorting List using quick sort");
}
private void sortListUsingMergeSort() {
System.out.println("Sorting List using merge sort");
}
}
使用看看:
/**
* 客户
*/
public class SortingMainV1 {
public static void main(String[] args) {
List list = Arrays.asList(new Integer[]{44,5,3,5,5,64,3});
SortingManagerV1 sm = new SortingManagerV1(list);
// Sorting using merge sort
sm.sortListBaseOnType(SortingTypeV1.MERGE_SORT);
System.out.println();
// Sorting using quick sort
sm.sortListBaseOnType(SortingTypeV1.QUICK_SORT);
}
}
现在我们要加入一种新的堆排序算法,那么V1的变化会有,首先在SortingTypeV1中加入一个新的类型,
然后打开SortingManagerV1类,修改sortListBaseOnType方法的if-else,添加一个新的分支,接着新增一个
方法来实现堆排序。(这儿的代码未实现,读者可以自行实现感受下)
整个过程我们会发现,这会修改已有类的方法,也就是会对已经经过严格测试验证的类产生变动,从而带来稳定性问题和测试成本问题。违反开闭原则。
好了,我们使用策略模式来实现。
首先,定义策略抽象,对应于这里就是排序策略。
/**
* 抽象排序策略
*/
public interface SortingStrategy {
void sortList(List list);
}
接下来,提供具体的策略算法,对应这里就是具体的排序算法。
/**
* 快速排序算法
*/
public class QuickSortStrategy implements SortingStrategy{
@Override
public void sortList(List list) {
System.out.println("Sorting List using quick sort");
}
}
/**
* 归并排序算法
*/
public class MergeSortStrategy implements SortingStrategy{
@Override
public void sortList(List list) {
System.out.println("Sorting List using merge sort");
}
}
然后,定义组合策略抽象的上下文,对应这里就是排序管理器
/**
* 使用策略模式解决
*/
public class SortingManagerV2 {
SortingStrategy sortingStrategy;
List<Integer> list;
public SortingManagerV2(SortingStrategy sortingStrategy, List<Integer> list) {
this.sortingStrategy = sortingStrategy;
this.list = list;
}
public void sortingList(){
System.out.println("===================================");
System.out.println("Sorting List based on Strategy");
System.out.println("===================================");
sortingStrategy.sortList(list);
}
public SortingStrategy getSortingStrategy() {
return sortingStrategy;
}
public void setSortingStrategy(SortingStrategy sortingStrategy) {
this.sortingStrategy = sortingStrategy;
}
}
最后,测试下
/**
* 客户
*/
public class SortingMainV2 {
public static void main(String[] args) {
List list = Arrays.asList(new Integer[]{44,5,3,5,5,64,3});
SortingManagerV2 sm = new SortingManagerV2(new MergeSortStrategy(), list);
sm.sortingList();
System.out.println();
sm.setSortingStrategy(new QuickSortStrategy());
sm.sortingList();
}
}
同样,现在加入一种新的堆排序算法,看看策略模式是如何应对的。
提供一种新的堆排序算法实现
/**
* 堆排序算法
*/
public class HeapSortStrategy implements SortingStrategy{
@Override
public void sortList(List list) {
System.out.println("Sorting List using heap sort");
}
}
然后就可以测试了
sm.setSortingStrategy(new HeapSortStrategy());
sm.sortingList();
没毛病。发现使用策略模式解决这种问题,真是好用的不得了。
不过,策略模式也有它的缺点。它会产生很多类出来,实际使用中,需要权衡考虑。
《Head First设计模式》
https://java2blog.com/strategy-design-pattern-java/
https://gitee.com/cq-laozhou/design-pattern