Scott's world.

设计模式-策略模式

Word count: 3.5kReading time: 13 min
2019/08/25 Share

设计模式-策略模式

模式动机

  • 在软件系统中,有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。
  • 除了提供专门的查找算法类之外,还可以在客户端程序中直接包含算法代码,这种做法更不可取,将导致客户端程序庞大而且难以维护,如果存在大量可供选择的算法时问题将变得更加严重。
  • 为了解决这些问题,可以定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法,在这里,每一个封装算法的类我们都可以称之为策略(Strategy),为了保证这些策略的一致性,一般会用一个抽象的策略类来做算法的定义,而具体每种算法则对应于一个具体策略类。

模式定义

策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。

策略模式把对象本身和运算规则区分开来,其功能非常强大,因为这个设计模式本身的核心思想就是面向对象编程的多形性的思想

模式结构

策略模式包含如下角色:

  • Context: 环境类
  • Strategy: 抽象策略类
  • ConcreteStrategy: 具体策略类

代码实现

现在我们来假想一个场景,也就是大家熟知的游戏背景,每一个英雄都有不同的名字,固定的4个技能即(Q,W,E,R),所以你很自然地联想到将这一个英雄作为一个抽象类

那我们开始吧

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Hero
{
protected String name;

protected abstract void Q();

protected abstract void W();

protected abstract void E();

protected abstract void R();

}

然后我们用两个英雄来实现这一个抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class HeroA extends Hero
{
public HeroA(String name)
{
this.name = name;
}

@Override
protected void Q()
{
System.out.println("嘲讽");
}

@Override
protected void W()
{
System.out.println("死亡之地");
}

@Override
protected void E()
{
System.out.println("不灭之握");
}

@Override
protected void R()
{
System.out.println("降维打击");
}

}

这样我们很快就写好了代码,但是现在产品经理要求再加入两个角色

HeroC(“嘲讽”,”烽火狼烟”,”不灭之握”,”降维打击”)

HeroB(“嘲讽”,”休养生息”,”龙抬头”,”降维打击”)

于是我们继续使用上面的方式来写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class HeroB extends Hero
{
public HeroB(String name)
{
this.name = name;
}

@Override
protected void Q()
{
System.out.println("嘲讽");
}

@Override
protected void W()
{
System.out.println("休养生息");
}

@Override
protected void E()
{
System.out.println("龙抬头");
}

@Override
protected void R()
{
System.out.println("降维打击");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class HeroC extends Hero
{
public HeroC(String name)
{
this.name = name;
}

@Override
protected void Q()
{
System.out.println("嘲讽");
}

@Override
protected void W()
{
System.out.println("烽火狼烟");
}

@Override
protected void E()
{
System.out.println("不灭之握");
}

@Override
protected void R()
{
System.out.println("降维打击");
}

}

写到这里,你应该就发现一些问题,那就是代码中已经出现相当多的重复代码,因为有一些技能是重复的,所以现在我们需要重新考虑设计架构.于是我们很自然地想到将每个技能都写成接口,有什么技能即使用接口,但是实现起来的时候,你会发现接口并不能实现代码的复用,每个实现接口的类还是必须自己实现.

于是想到这里,我们就可以使用今天的主角策略模式,遵循设计的原则,找出应用中可能需要变化的部分,把它们独立出来,不要和那些不需要变化的代码混在一起.

我们发现每个英雄的Q,W,E,R都是有可能变化的,于是我们必须将这一部分独立出来

最后再根据另一个设计原则:针对接口(超类型)编程,而不是针对实现编程,于是我们把代码修改成这样

1
2
3
4
public interface QBehavior
{
void Q();
}
1
2
3
4
public interface WBehavior
{
void w();
}
1
2
3
4
public interface EBehavior
{
void E();
}
1
2
3
4
public interface RBehavior
{
void R();
}
1
2
3
4
5
6
7
8
public Q1 implements QBehavior
{
@Override
public void Q()
{
System.out.println("嘲讽");
}
}
1
2
3
4
5
6
7
8
public W1 implements WBehavior
{
@Override
public void W()
{
System.out.println("休养生息");
}
}
1
2
3
4
5
6
7
8
public W2 implements WBehavior
{
@Override
public void W()
{
System.out.println("烽火狼烟");
}
}
1
2
3
4
5
6
7
8
public E1 implements EBehavior
{
@Override
public void E()
{
System.out.println("不灭之握");
}
}
1
2
3
4
5
6
7
8
public R1 implements RBehavior
{
@Override
public void R()
{
System.out.println("降维打击");
}
}

剩余的技能树就可以像这样简单的扩展,并且每个技能可以有自己的实现方式,我就不一一展示了

这时候需要对Hero的代码做出改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public abstract class Hero
{
protected String name;

protected QBhavior Q;
protected WBehavior W;
protected EBehavior E;
protected RBehavior R;

public Hero setQBehavior(QBehavior Q)
{
this.Q = Q;
return this;
}

public Hero setWBehavior(WBehavior W)
{
this.W = W;
return this;
}

public Hero setEBehavior(EBehavior E)
{
this.E = E;
return this;
}

public Hero setRBehavior(RBehavior R)
{
this.R = R;
return this;
}

protected void Q()
{
Q.Q();
}

protected void W()
{
W.W();
}

protected void R()
{
R.R();
}

protected void E()
{
E.E();
}

}

现在每个角色只需要一个name了:

1
2
3
4
5
6
7
public class HeroA extends Hero
{
public HeroA(String name)
{
this.name = name;
}
}

最后如果我们需要一个新的角色只需要像下面就这样就可以得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test
{
public static void main(String[] args)
{

Hero HeroA = new HeroA("A");

HeroA.setQBehavior(new Q1())//
.setWBehavior(new W1())//
.setEBehavior(new E1())//
.setRBehavior(new R1());
System.out.println(HeroA.name + ":");
HeroA.Q();
HeroA.W();
HeroA.E();
HeroA.R();
}
}

所以说现在经过我们的修改,现在所有的技能都能做到100%的复用,并且随便产品经理需要什么样的英雄,对于我们来说只需要动态设置一下技能和展示方式。

现在你已经学会了策略模式,现在我们回到定义,定义上的算法族:其实就是上述例子的技能;定义上的客户:其实就是HeroA,HeroB…;我们已经定义了一个算法族(各种技能),且根据需求可以进行相互替换,算法(各种技能)的实现独立于客户(英雄)。现在是不是很好理解策略模式的定义了。

模式分析

  • 策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。
  • 策略模式通常把一个系列的算法封装到一系列的策略类里面,作为一个抽象策略类的子类。用一句话来说,就是“准备一组算法,并将每一个算法封装起来,使得它们可以互换”。
  • 在策略模式中,应当由客户端自己决定在什么情况下使用什么具体策略角色。
  • 策略模式仅仅封装算法,提供新算法插入到已有系统中,也可以实现算法的更新和删除,这样极大便捷了更新流程,而且策略模式并不决定在何时使用何种算法,算法的选择由具体客户来决定.这在一定程度上提高了系统的灵活性,但是客户端需要理解所有具体策略类之间的区别,以便选择合适的算法,这也是策略模式的缺点之一,在一定程度上增加了客户端的使用难度。

模式优缺点

优点

  • 相关算法系列 Strategy类层次为Context定义了一系列的可供重用的算法或行为。 继承有助于析取出这些算法中的公共功能。
  • 提供了可以替换继承关系的办法: 继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个Context类的子类,从而给它以不同的行为。但这会将行为硬行编制到 Context中,而将算法的实现与Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。最后你得到一堆相关的类 , 它们之间的唯一差别是它们所使用的算法或行为。 将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。
  • 消除了一些if else条件语句 :Strategy模式提供了用条件语句选择所需的行为以外的另一种选择。当不同的行为堆砌在一个类中时 ,很难避免使用条件语句来选择合适的行为。将行为封装在一个个独立的Strategy类中消除了这些条件语句。含有许多条件语句的代码通常意味着需要使用Strategy模式。
  • 实现的选择 Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间 /空间权衡取舍要求从不同策略中进行选择

缺点

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类

  • Strategy和Context之间的通信开销

    无论各个ConcreteStrategy实现的算法是简单还是复杂, 它们都共享Strategy定义的接口。因此很可能某些 ConcreteStrategy不会都用到所有通过这个接口传递给它们的信息;简单的 ConcreteStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样问题 , 那么将需要在Strategy和Context之间更进行紧密的耦合。

  • 策略模式将造成产生很多策略类

    可以通过使用享元模式在一定程度上减少对象的数量。 增加了对象的数目 Strategy增加了一个应用中的对象的数目。有时你可以将 Strategy实现为可供各Context共享的无状态的对象来减少这一开销。任何其余的状态都由 Context维护。Context在每一次对Strategy对象的请求中都将这个状态传递过去。共享的 Strategy不应在各次调用之间维护状态

模式适用

当存在以下情况时使用策略模式

  • 许多相关的类仅仅是行为有异。 “策略”提供了一种用多个行为中的一个行为来配置一个类的方法。即一个系统需要动态地在几种算法中选择一种。
  • 需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间 /时间权衡的算法。当这些变体实现为一个算法的类层次时 ,可以使用策略模式。
  • 算法使用客户不应该知道的数据可使用策略模式以避免暴露复杂的、与算法相关的数据结构。
  • 一个类定义了多种行为 , 并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。

模式扩展

vs 状态模式

大家可能发现策略模式与其他设计模式比较起来是非常类似的

而策略模式和状态模式最大的区别

  • 就是策略模式只是的条件选择只执行一次,而状态模式是随着实例参数(对象实例的状态)的改变不停地更改执行模式。

    换句话说,策略模式只是在对象初始化的时候更改执行模式,而状态模式是根据对象实例的周期时间而动态地改变对象实例的执行模式。

vs 工厂模式

  • 工厂模式是创建型模式 ,它关注对象创建,提供创建对象的接口. 让对象的创建与具体的使用客户无关。
  • 策略模式是对象行为型模式 ,它关注行为和算法的封装 。它定义一系列的算法,把每一个算法封装起来, 并且使它们可相互替换。使得算法可独立于使用它的客户而变化

用一个旅行的例子来说明他们两的区别:
策略模式的做法:有几种方案供你选择旅行,选择火车好呢还是骑自行车,完全有客户自行决定去构建旅行方案(比如你自己需要去买火车票,或者机票)。

而工厂模式是你决定哪种旅行方案后,不用关注这旅行方案怎么给你创建,也就是说你告诉我方案的名称就可以了,然后由工厂代替你去构建具体方案(工厂代替你去买火车票)。

参考

https://blog.csdn.net/hguisu/article/details/7558249

https://blog.csdn.net/lmj623565791/article/details/24116745

https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/strategy.html

CATALOG
  1. 1. 设计模式-策略模式
    1. 1.1. 模式动机
    2. 1.2. 模式定义
    3. 1.3. 模式结构
    4. 1.4. 代码实现
    5. 1.5. 模式分析
    6. 1.6. 模式优缺点
      1. 1.6.1. 优点
      2. 1.6.2. 缺点
    7. 1.7. 模式适用
    8. 1.8. 模式扩展
      1. 1.8.1. vs 状态模式
      2. 1.8.2. vs 工厂模式
    9. 1.9. 参考