目录
1 策略模式简介
2 策略模式的结构与实现
3 策略模式的应用实例
4 策略模式的应用场景
5 策略模式的扩展
6 状态模式和策略模式的区别
1 策略模式简介
名称 | Strategy |
结构 | 图2如下: |
参与者 |
|
意图 | 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。 |
适用性 |
|
策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
策略模式的主要优点如下。
- 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。
- 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
- 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
- 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
其主要缺点如下。
- 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
- 策略模式造成很多的策略类。
2 策略模式的结构与实现
策略模式是准备一组算法,并将这组算法封装到一系列的策略类里面,作为一个抽象策略类的子类。策略模式的重心不是如何实现算法,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性。通常用类似key-value键值对(key为标志,value为函数指针等形式的策略)的组织形式保存在map等结构中,使用时根据key查找到value然后执行策略。现在我们来分析其基本结构和实现方法。
1. 模式的结构
策略模式的主要角色如下。
- 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
其结构图如图 1 所示。
图1 策略模式的结构图
2. 模式的实现
策略模式的实现代码如下:
package strategy;
public class StrategyPattern
{
public static void main(String[] args)
{
Context c=new Context();
Strategy s=new ConcreteStrategyA();
c.setStrategy(s);
c.strategyMethod();
System.out.println("-----------------");
s=new ConcreteStrategyB();
c.setStrategy(s);
c.strategyMethod();
}
}
//抽象策略类
interface Strategy
{
public void strategyMethod(); //策略方法
}
//具体策略类A
class ConcreteStrategyA implements Strategy
{
public void strategyMethod()
{
System.out.println("具体策略A的策略方法被访问!");
}
}
//具体策略类B
class ConcreteStrategyB implements Strategy
{
public void strategyMethod()
{
System.out.println("具体策略B的策略方法被访问!");
}
}
//环境类
class Context
{
private Strategy strategy;
public Strategy getStrategy()
{
return strategy;
}
public void setStrategy(Strategy strategy)
{
this.strategy=strategy;
}
public void strategyMethod()
{
strategy.strategyMethod();
}
}
程序运行结果如下:
具体策略A的策略方法被访问!
-----------------
具体策略B的策略方法被访问!
3 策略模式的应用实例
【例1】策略模式在“大闸蟹”做菜中的应用。
分析:关于大闸蟹的做法有很多种,我们以清蒸大闸蟹和红烧大闸蟹两种方法为例,介绍策略模式的应用。
首先,定义一个大闸蟹加工的抽象策略类(CrabCooking),里面包含了一个做菜的抽象方法 CookingMethod();然后,定义清蒸大闸蟹(SteamedCrabs)和红烧大闸蟹(BraisedCrabs)的具体策略类,它们实现了抽象策略类中的抽象方法;由于本程序要显示做好的结果图(点此下载要显示的结果图),所以将具体策略类定义成 JLabel 的子类;最后,定义一个厨房(Kitchen)环境类,它具有设置和选择做菜策略的方法;客户类通过厨房类获取做菜策略,并把做菜结果图在窗体中显示出来,图 2 所示是其结构图。
图2 大闸蟹做菜策略的结构图
程序代码如下:
package strategy;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class CrabCookingStrategy implements ItemListener
{
private JFrame f;
private JRadioButton qz,hs;
private JPanel CenterJP,SouthJP;
private Kitchen cf; //厨房
private CrabCooking qzx,hsx; //大闸蟹加工者
CrabCookingStrategy()
{
f=new JFrame("策略模式在大闸蟹做菜中的应用");
f.setBounds(100,100,500,400);
f.setVisible(true);
f.setResizable(false);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
SouthJP=new JPanel();
CenterJP=new JPanel();
f.add("South",SouthJP);
f.add("Center",CenterJP);
qz=new JRadioButton("清蒸大闸蟹");
hs=new JRadioButton("红烧大闸蟹");
qz.addItemListener(this);
hs.addItemListener(this);
ButtonGroup group=new ButtonGroup();
group.add(qz);
group.add(hs);
SouthJP.add(qz);
SouthJP.add(hs);
//---------------------------------
cf=new Kitchen(); //厨房
qzx=new SteamedCrabs(); //清蒸大闸蟹类
hsx=new BraisedCrabs(); //红烧大闸蟹类
}
public void itemStateChanged(ItemEvent e)
{
JRadioButton jc=(JRadioButton) e.getSource();
if(jc==qz)
{
cf.setStrategy(qzx);
cf.CookingMethod(); //清蒸
}
else if(jc==hs)
{
cf.setStrategy(hsx);
cf.CookingMethod(); //红烧
}
CenterJP.removeAll();
CenterJP.repaint();
CenterJP.add((Component)cf.getStrategy());
f.setVisible(true);
}
public static void main(String[] args)
{
new CrabCookingStrategy();
}
}
//抽象策略类:大闸蟹加工类
interface CrabCooking
{
public void CookingMethod(); //做菜方法
}
//具体策略类:清蒸大闸蟹
class SteamedCrabs extends JLabel implements CrabCooking
{
private static final long serialVersionUID=1L;
public void CookingMethod()
{
this.setIcon(new ImageIcon("src/strategy/SteamedCrabs.jpg"));
this.setHorizontalAlignment(CENTER);
}
}
//具体策略类:红烧大闸蟹
class BraisedCrabs extends JLabel implements CrabCooking
{
private static final long serialVersionUID=1L;
public void CookingMethod()
{
this.setIcon(new ImageIcon("src/strategy/BraisedCrabs.jpg"));
this.setHorizontalAlignment(CENTER);
}
}
//环境类:厨房
class Kitchen
{
private CrabCooking strategy; //抽象策略
public void setStrategy(CrabCooking strategy)
{
this.strategy=strategy;
}
public CrabCooking getStrategy()
{
return strategy;
}
public void CookingMethod()
{
strategy.CookingMethod(); //做菜
}
}
程序运行结果如图 3 所示。
图3 大闸蟹做菜结果
【例2】用策略模式实现从韶关去婺源旅游的出行方式。
分析:从韶关去婺源旅游有以下几种出行方式:坐火车、坐汽车和自驾车,所以该实例用策略模式比较适合,图 4 所示是其结构图。
图4 婺源旅游结构图
4 策略模式的应用场景
在现实生活中常常遇到实现某种目标存在多种策略可供选择的情况,例如,出行旅游可以乘坐飞机、乘坐火车、骑自行车或自己开私家车等,超市促销可以釆用打折、送商品、送积分等方法。
在软件开发中也常常遇到类似的情况,当实现某一个功能存在多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能,如数据排序策略有冒泡排序、选择排序、插入排序、二叉树排序等。
如果使用多重条件转移语句实现(即硬编码),不但使条件语句变得很复杂,而且增加、删除或更换算法要修改原代码,不易维护,违背开闭原则。如果采用策略模式就能很好解决该问题。
策略模式在很多地方用到,如 Java SE 中的容器布局管理就是一个典型的实例,Java SE 中的每个容器都存在多种布局供用户选择。在程序设计中,通常在以下几种情况中使用策略模式较多。
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
- 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
5 策略模式的扩展
在一个使用策略模式的系统中,当存在的策略很多时,客户端管理所有策略算法将变得很复杂,如果在环境类中使用策略工厂模式来管理这些策略类将大大减少客户端的工作复杂度,其结构图如图 5 所示。
图5 策略工厂模式的结构图
6 状态模式和策略模式的区别
----------------------观点一----------------------------
状态模式与策略模式区别
1、类图、类的结构一样:
状态模式:Context环境类、State类(抽象状态类、具体状态类)
策略模式:Context环境类、Strategy类(抽象策略类、具体策略类)
两种模式都是行为型模式,UML图相同,都是将核心类(State类或Strategy类)注入到Context类中,在客户端通过操作Context环境类间接操作核心类(State类或Strategy类),巧妙的在客户端屏蔽核心类。
2、表达意义稍有不同:
状态可以看作是Context类的一个内在属性,是必不可少的,新建Context类对象时,Context类的初始化函数中就要初始化状态属性;
策略不是Context的属性,是Context类调用的一个外界的东西;
所以,一般核心类(State类或Strategy类)是内在属性的时候是状态模式,是外界东西的时候是策略模式。
3、代码逻辑稍有不同:状态模式是含有“状态切换”逻辑(最大的不同)
状态模式中一定含有“状态切换”逻辑,不管是在ConcreteState类中还是在Context类中,状态类中一定含有“状态切换”代码;
策略模式只是简单的在客户端切换策略,核心类(Strategy类)中没有“切换”逻辑。
其实,正是因为第二条,State是Context内在属性,所以有切换逻辑,Strategy是外在东西,所有没有切换逻辑。
所以,存在核心类(State类或Strategy类)切换逻辑的是状态模式,不存在的是策略模式。
----------------------观点二----------------------------
策略模式与状态模式在实现上有共同之处,都是把不同的情形抽象为统一的接口来实现,就放在一起进行记录。2个模式的UML建模图基本相似,区别在于状态模式需要在子类实现与context相关的一个状态行为。
状态模式的的思想是,状态之间的切换,在状态A执行完毕后自己控制状态指向状态B。状态模式是不停的切换状态执行。
策略模式的思想上是,考虑多种不同的业务规则将不同的算法封装起来,便于调用者选择调用。策略模式只是条件选择执行一次。
策略模式
- Strategy: 定义所有支持的算法的公共接口抽象类.
- ConcreteStrategy: 封装了具体的算法或行为,继承于Strategy
- Context: 用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用。
状态模式
- State: 抽象状态类,定义一个接口以封装与context的一个状态相关的行为
- ConcreteState: 具体状态,每一子类实现一个与Context的一个状态相关的行为
- Context: 维护一个ConcreteState子类的实例,这个实例定义当前的状态。
使用场景:
状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。
策略模式的Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。在实践中,我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
状态模式和策略模式的比较
两个模式的实现类图虽然一致,但是实现目的不一样!
首先知道,策略模式是一个接口的应用案例,一个很重要的设计模式,简单易用,策略模式一般用于单个算法的替换,客户端事先必须知道所有的可替换策略,由客户端去指定环境类需要哪个策略,注意通常都只有一个最恰当的策略(算法)被选择。其他策略是同级的,可互相动态的在运行中替换原有策略。
而状态模式的每个状态子类中需要包含环境类(Context)中的所有方法的具体实现——条件语句。通过把行为和行为对应的逻辑包装到状态类里,在环境类里消除大量的逻辑判断,而不同状态的切换由继承(实现)State的状态子类去实现,当发现修改的当前对象的状态不是自己这个状态所对应的参数,则各个状态子类自己给Context类切换状态(有职责链模式思想)!且客户端不直接和状态类交互,客户端不需要了解状态!(和策略不一样),策略模式是直接依赖注入到Context类的参数进行选择策略,不存在切换状态的操作,客户端需要了解策略!
联系:状态模式和策略模式都是为具有多种可能情形设计的模式,把不同的处理情形抽象为一个相同的接口(抽象类),符合对开闭原则,且策略模式更具有一般性,在实践中,可以用策略模式来封装几乎任何类型的规则,只要在分析过程中听到需要在不同实践应用不同的业务规则,就可以考虑使用策略模式处理,在这点上策略模式是包含状态模式的功能的。
小结:状态模式的使用场景是什么?
状态模式主要解决的是(目的or意图):控制一个对象内部的状态转换的条件表达式过于复杂时的情况,且客户端调用之前不需要了解具体状态。它把状态的判断逻辑转到表现不同状态的一系列类当中,可以把复杂的判断逻辑简化。维持开闭原则,方便维护
,还有重要一点下面会总结,状态模式是让各个状态对象自己知道其下一个处理的对象是谁!即在状态子类编译时在代码上就设定好了!
状态模式的优缺点都是什么?
优点,前面说了很多了……
- 状态模式使得代码中复杂而庸长的逻辑判断语句问题得到了解决,而且状态角色将具体的状态和他对应的行为及其逻辑判断封装了起来,这使得增加一种新的状态显得十分简单。
- 把容易出错的if-else语句在环境类 or 客户端中消除,方便维护。
- 每一个状态类都符合“开闭”原则——对状态的修改关闭,对客户端的扩展开放,可以随时增加新的Person的状态,或者删除。
- State类在只有行为需要抽象时,就用接口,有其他共同功能可以用抽象类,这点和其他一些(策略)模式类似。
缺点:
使用状态模式时,每个状态对应一个具体的状态类,使结构分散,类的数量变得很多!使得程序结构变得稍显复杂,阅读代码时相对之前比较困难,不过对于优秀的研发人员来说,应该是微不足道的。因为想要获取弹性!就必须付出代价!除非我们的程序是一次性的!用完就丢掉……如果不是,那么假设有一个系统,某个功能需要很多状态,如果不使用状态模式优化,那么在环境类(客户端类)里会有大量的整块整块的条件判断语句!
Strategy模式有下面的一些优点:
1) 相关算法系列 Strategy类层次为Context定义了一系列的可供重用的算法或行为。 继承有助于析取出这些算法中的公共功能。
2) 提供了可以替换继承关系的办法: 继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个Context类的子类,从而给它以不同的行为。但这会将行为硬行编制到 Context中,而将算法的实现与Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。最后你得到一堆相关的类 , 它们之间的唯一差别是它们所使用的算法或行为。 将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。
3) 消除了一些if else条件语句 :Strategy模式提供了用条件语句选择所需的行为以外的另一种选择。当不同的行为堆砌在一个类中时 ,很难避免使用条件语句来选择合适的行为。将行为封装在一个个独立的Strategy类中消除了这些条件语句。含有许多条件语句的代码通常意味着需要使用Strategy模式。
4) 实现的选择 Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间 /空间权衡取舍要求从不同策略中进行选择。
Strategy模式缺点:
1)客户端必须知道所有的策略类,并自行决定使用哪一个策略类: 本模式有一个潜在的缺点,就是一个客户要选择一个合适的Strategy就必须知道这些Strategy到底有何不同。此时可能不得不向客户暴露具体的实现问题。因此仅当这些不同行为变体与客户相关的行为时 , 才需要使用Strategy模式。
2 ) Strategy和Context之间的通信开销 :无论各个ConcreteStrategy实现的算法是简单还是复杂, 它们都共享Strategy定义的接口。因此很可能某些 ConcreteStrategy不会都用到所有通过这个接口传递给它们的信息;简单的 ConcreteStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样问题 , 那么将需要在Strategy和Context之间更进行紧密的耦合。
3 )策略模式将造成产生很多策略类:可以通过使用享元模式在一定程度上减少对象的数量。 增加了对象的数目 Strategy增加了一个应用中的对象的数目。有时你可以将 Strategy实现为可供各Context共享的无状态的对象来减少这一开销。任何其余的状态都由 Context维护。Context在每一次对Strategy对象的请求中都将这个状态传递过去。共享的 Strategy不应在各次调用之间维护状态。
7 模板方法和策略模式的区别
模板方法模式的主要思想:定义一个算法流程,将一些特定步骤的具体实现、延迟到子类。使得可以在不改变算法流程的情况下,通过不同的子类、来实现“定制”流程中的特定的步骤。策略模式的主要思想:使不同的算法可以被相互替换,而不影响客户端的使用。
在思想和意图上看,模板方法更加强调:
1)定义一条线(算法流程),线上的多个点是可以变化的(具体实现在子类中完成),线上的多个点一定是会被执行的,并且一定是按照特定流程被执行的。
2)算法流程只有唯一的入口,对于点的访问是受限的【通常用受保护的虚函数来定义可变点】。
策略模式更注重于: 一个“策略”是一个 整体的(完整的) 算法,算法是可以被整体替换的。而模板方法只能被替换其中的特定点,算法流程是固定不可变的。
策略模式建议“优先使用对象组合,而不是继承”。
模式 | 优点 | 缺点 |
---|---|---|
策略 | 横向扩展性好,灵活性高 | 客户端需要知道全部策略,若策略过多会导致复杂度升高 |
模板 | 可维护性好,纵向扩展性好 | 耦合性较高,子类无法影响父类公用模块代码 |
参考资料:
《设计模式之禅(第2版)》
《设计模式:可复用面向对象软件的基础》
《Java设计模式:23种设计模式全面解析(超级详细)》:Java设计模式:23种设计模式全面解析(超级详细)
发布评论