flyEn'blog

设计模式(一)策略模式

本文为看《Head First设计模式》书后整理的技术分享第一篇。

这是一本比较轻松的讲述设计模式的书,教我们如何利用设计模式复用其他人的经验,如何利用设计模式提高代码的可维护性和可扩展性。

项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。

Head First设计模式里陆续的介绍了策略模式、观察者模式、装饰者模式、工厂方法模式、抽象工厂模式、单件模式、命令模式、适配器模式、外观模式、模板方法模式、迭代器模式、组合模式、状态模式、代理模式,在介绍各种模式的期间,用简单的应用场景、通俗的语言引导读者去思考这些模式是如何利用和遵循相应OO原则的,然后再清晰的总结出每种模式的定义。

这是一本体验式学习的书,不是一本参考书。

没有包含所有的设计模式。是从GoF模式中,取出更重要的一部分模式,作为本书的焦点,并确保读者能够真正地、深入地、彻底地了解如何使用这些模式,以及何时使用这些模式。

策略模式

设计原则

  • 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
  • 针对接口编程,不针对实现编程。
  • 多用组合,少用继承。

举例

书中以一个例子来展开策略模式,方便理解。

👉模拟鸭子游戏:游戏中会出现各种鸭子,一边游泳戏水,一边呱呱叫,设计一个鸭子超类,并让各种鸭子继承此超类。

所有的鸭子都会呱呱叫(Quack)也会游泳(Swim)所以由超类负责处理这部分的代码实现。每种鸭子的外观都不同,所以display()方法是抽象的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class abstract Duck{

public void quack(){};
public void swim(){};
public abstract void display();
}

class MallardDuck extends Duck{
@Override
public void display(){
//外观是绿头
}
}

class RedheadDuck extends Duck{
@Override
public void display(){
//外观是红头
}
}

但很快,需求方又希望鸭子能飞。

设想:只需要在Duck类中加上fly()方法,然后让所有鸭子都会继承fly()

1
2
3
4
5
6
class abstract Duck{
public void quack(){};
public void swim(){};
public void fly(){};
public abstract void display();
}

但是,可怕的问题发生了:发现很多的“橡皮鸭子”在天上飞。所以并非Duck所有的子类都会飞。像橡皮鸭子这样没有生命的鸭子。

当涉及“维护”时,为了“复用”目的而使用继承,结局并不完美

这时候想到了继承。可以把橡皮鸭类的fly()方法覆盖掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RubberDuck extends Duck{
@Override
public void quack(){
//吱吱叫
}
@Override
public void display(){
//橡皮鸭
}
@Override
public void fly(){
//覆盖,变成什么事都不做
}
}

这里会出现个问题:利用继承来提供Duck的行为,会导致以下哪些缺点?

☑️ 代码在多个子类中重复。

☑️ 运行时的行为不容易改变。

☑️ 很难知道所有的鸭子的全部行为。

☑️ 改变会牵一发动全身,造成其他鸭子不想要的改变。

认识到继承可能不是答案,每当有新的鸭子子类出现,他就要被迫检查并可能需要覆盖fly()和quark()。所以我们需要一个更清晰的方法,让“某些”鸭子类型可飞或可叫。
我们可以把fly()从超类中取出来,放进一个“Flyable接口”中,这么一来,只有会飞的鸭子才实现此接口,同样的方式,也可以设计一个“Quackable接口”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Flyable{
public void fly();
}
interface Quackable{
public void quack();
}
class abstract Duck{
public void swim(){};
public abstract void display();
}
class MallardDuck extends Duck implements Flyable,Quackable{
@Override
public void display() {
}
@Override
public void fly() {
}
@Override
public void quack(){
}
}

然后,你会发现虽然FlyableQuackable可以解决一部分问题,但是却造成了代码无法复用,这只能算是从一个噩梦跳进另一个噩梦。甚至,在会飞的鸭子中,飞行的动作可能还有多种变化。

不管当初软件设计得多好,一段时间之后,总是需要成长与改变,否则软件就会“死亡”

把问题归零……

现在我们知道使用继承并不能很好地解决问题,因为鸭子的行为在子类里不断改变,并且让所有的子类都有这些行为是不恰当的,FlyableQuackable接口一开始似乎还不错,但是java接口不具有实现代码,所以继承接口无法达到代码的复用。这就意味着:无论何时你需要修改某个行为,你必须得往下追踪并在每一个定义此行为的类中修改它,一不小心,可能会造成新的错误。

🌟第一个设计原则:找出应用中可能需要变化之处,把它们独立起来,不要和那些不需要变化的代码混在一起

把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要变化的其他部分了。

分开变化和不会变化的部分

我们准备建立两组类(完全远离Duck类),一个是“fly”相关的,一个是“quack”相关的,每一组类将实现各自的动作。分别实现“呱呱叫”,“吱吱叫”,“安静(不叫)”

设置鸭子的行为

我们希望一切能有弹性,我们还想能够“指定”行为到鸭子的实例。比方说,我们想要产生一个新的绿头鸭实例,并指定特定“类型”的飞行行为给它,干脆顺便让鸭子的行为可以动态的改变,我们应该在鸭子类中包含设定行为的方法, 这样可以在“运行时”动态地“改变”绿头鸭的飞行行为

🌟第二个设计原则:针对接口编程,而不是针对实现编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface  FlyBehavior{
void fly();
}

class FlyWithWings implements FlyBehavior{

@Override
public void fly() {
//实现鸭子的飞行动作
}
}
class FlyNoWay implements FlyBehavior{

@Override
public void fly() {
//什么都不做,不会飞
}
}

利用接口代表每个行为FlyBehaviorQuackBehavior,而行为的每个实现都将实现其中的一个接口。

所以这次鸭子类不会负责实现FlyingQuacking接口,反而是由我们制造一组其他类专门实现FlyBehaviorQuackBehavior这就称为“行为”类。由行为类而不是Duck类来实现行为接口。

我们的新设计中,鸭子的子类将使用接口(FlyBehavior与QuackBehavior)所表示的行为,所以实际的“实现”不会被绑死在鸭子的子类中。

这里有一个问题:为什么非要把FlyBehavior设计成接口,为何不使用抽象
超类,这样不就可以使用多态了吗?

“针对接口编程”真正的意思是“针对超类型编程”

这里所谓的“接口”有多个含义,接口是一个“概念”,也是一种java的interface构造。“针对接口编程”,关键就在多态,利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上。

实现鸭子的行为

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
interface QuackBehavior{
void quack();
}

class Quack implements QuackBehavior{

@Override
public void quack() {
//实现鸭子呱呱叫
}
}
class Squeak implements QuackBehavior{

@Override
public void quack() {
//橡皮鸭吱吱叫
}
}
class MuteQuack implements QuackBehavior{

@Override
public void quack() {
//什么都不做,不会叫
}
}

这样的设计,可以让飞行和呱呱叫的动作被其他的对象复用,因为这些行为已经与鸭子类无关了。
而我们可以新增一些行为,不会影响到既有的行为类,也不会影响“使用”到飞行行为的鸭子类。

整合鸭子的行为
鸭子现在会将飞行和呱呱叫的动作“委托”别人处理,而不是使用定义在Duck类(或子类)内的呱呱叫和飞行方法。
首先在Duck类中“加入两个实例变量”,分别为“flyBehavior”,“quackBehavior”,
声明为接口类型(而不是具体类实现类型),每个鸭子对象都会动态地设置这些变量以在运行时应用正确的行为类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Duck{
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;

public void performQuack(){
quackBehavior.quack();
}
public void performFly(){
flyBehavior.fly();
}
public abstract void display();
public void swim(){
}
}

更多的整合
如何设定flyBehaviorquackBehavior的实例变量。
MallardDuck类:

1
2
3
4
5
6
7
8
9
10
11
12
class MallardDuck extends Duck{

public MallardDuck(){
quackBehavior=new Quack(); //
flyBehavior=new FlyWithWings();
}

@Override
public void display() {
System.out.println("I'm a real Mallard duck");
}
}

前面我们说过将不对具体实现编程,但是我们在构造器中却实例化了具体的Quack和FlyWithWings类。在本书的后续内容中,我们会有更多的模式可用,就可以修正这一点了。

动态设定行为
在Duck类中,加入两个新方法:

1
2
3
4
5
6
public void setFlyBehavior(FlyBehavior fb){
flyBehavior=fb;
}
public void setQuackBehavior(QuackBehavior qb){
quackBehavior=qb;
}

我们不再把鸭子的行为说成“一组行为”,我们开始把行为想成是“一族算法”。算法代表鸭子能做的事(不同的叫法和飞行法)。

“有一个”可能比“是一个”更好
“有一个”:每一个鸭子都有一个FlyBehavior和一个QuackBehavior,好将飞行和呱呱叫委托给它们代为处理。
当你将两个类结合起来使用,如同本例一般,这就是组合,这种做法和“继承”不同的地方在于,鸭子的行为不是继承来的,而是和适当的行为对象“组合”来的。

🌟第三个设计原则:多用组合,少用继承

总结定义

策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户

实际项目中的应用🔺

以下介绍了策略模式在实际项目中的运用。

这个模式涉及到三个角色:

img

  • 环境(Context)角色:持有一个策略的引用
  • 抽象策略(Strategy)角色:是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
  • 具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。

策略模式模板代码

上下文类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Context {
//持有一个具体策略的对象
private Strategy strategy;
/**
* 构造函数,传入一个具体策略对象
* @param strategy 具体策略对象
*/
public Context(Strategy strategy){
this.strategy = strategy;
}
/**
* 策略方法
*/
public void contextInterface(){
strategy.algorithmInterface();
}
}

抽象策略接口类:

1
2
3
4
5
6
public interface Strategy {
/**
* 策略方法
*/
public void algorithmInterface();
}

具体策略类:

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
public class ConcreteStrategyA implements Strategy {

@Override
public void algorithmInterface() {
//相关的业务
}

}

public class ConcreteStrategyB implements Strategy {

@Override
public void algorithmInterface() {
//相关的业务
}

}

public class ConcreteStrategyC implements Strategy {

@Override
public void algorithmInterface() {
//相关的业务
}
}

应用层:

1
2
3
4
//选择使用的策略
Strategy s = new ConcreteStrategyA();
Context context = new Context(s);
context.ontextInterface();

策略模式的优缺点和适用场景

优点:1.算法可以自由切换;2.避免使用多重条件判断;3.扩展性良好;

缺点:1.策略类会增多;2.所有策略类都需要对外暴露;

适用场景:1.当一个系统中有许多类,它们之间的区别仅在于它们的行为,希望动态地让一个对象在许多行为中选择一种行为时。2.当一个系统需要动态地在几种算法中选择一种时。3.当一个对象有很多的行为,不想使用多重的条件选择语句来选择使用哪个行为时。

介绍一些应用场景

  1. java对象排序中的应用

    比如我们需要控制某个类的次序,而该类本身不支持排序,那么可建立一个该类的比较器来排序,这个比较器只需要实现Comparator接口即可。通过实现Comparator类来创建一个比较器,然后通过该比较器对类进行排序。Comparator接口其实就是一种策略模式的实践。

    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
    // 抽象策略类
    public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
    }
    // 具体策略类 SortComparator
    public class SortComparator implements Comparator {

    @Override
    public int compare(Object o1, Object o2) {
    Student student1 = (Student) o1;
    Student student2 = (Student) o2;
    return student1.getAge() - student2.getAge();
    }
    }
    // 策略上下文
    public class Client {

    public static void main(String[] args) {

    Student stu[] = {
    new Student("张三" ,23),
    new Student("李四" ,26),
    new Student("王五" ,22)};
    Arrays. sort(stu,new SortComparator());
    System.out.println(Arrays.toString(stu));

    List<Student> list = new ArrayList<>(3);
    list.add( new Student("zhangsan" ,31));
    list.add( new Student("lisi" ,30));
    list.add( new Student("wangwu" ,35));
    Collections. sort(list,new SortComparator());
    System.out.println(list);

    }

    }

还有商场促销活动,打折、满减等;出行方式,自行车、汽车等,每一种出行方式都是一个策略;Java AWT中的LayoutManager(布局管理器)…

实战案例

👉参考

在Spring项目中引入策略模式来解决一个通过不同的方式通知用户的场景。

比如用户注册、忘记密码或其他推送信息,我们需要给用户发送信息,可以通过电子邮件、电话、微信、短信等方式通知用户。

比较low的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void notice(String noticeWay, String userId) {
if ("EMAIL".equals(noticeWay)) {
// TODO 通过电子邮件通知用户具体逻辑
System.out.println("通过电子邮件通知用户具体逻辑");

} else if ("PHONE".equals(noticeWay)) {
// TODO 通过电话通知用户具体逻辑
System.out.println("通过电话通知用户具体逻辑");

} else if ("SMS".equals(noticeWay)) {
// TODO 通过短信通知用户具体逻辑
System.out.println("通过短信通知用户具体逻辑");

} else if ("WECHAT".equals(noticeWay)) {
// TODO 通过微信通知用户具体逻辑
System.out.println("通过微信通知用户具体逻辑");

}
// ...
}

这样的代码,每新增一个通知策略,都要加一层 else if,然后再里面写具体的逻辑。

使得代码难以维护,臃肿不易扩展

然后可以使用策略模式来解决这个问题。

img

代码如下:

  1. 策略工厂

    通知策略工厂,用于获取具体通知策略Bean对象,比如微信通知策略对象、邮件通知策略对象。

    因为我们使用Spring管理Bean,所以我们可以把通知方式和Bean对象维护中在一个Map中里。

    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
    package com.liuyanzhao.sens.demo.factory;
    import com.liuyanzhao.sens.demo.strategey.NoticeStrategy;
    import org.springframework.stereotype.Service;
    import java.util.HashMap;
    import java.util.Map;
    /**
    * 通知策略工厂
    *
    * @author 言曌
    * @date 2020/8/28 12:17 上午
    */
    @Service
    public class NoticeStrategyFactory {
    /**
    * 存储通知方式和策略Bean关系
    */
    public static Map<String, NoticeStrategy> STRATEGY_MAP = new HashMap<>();
    /**
    * 注册策略Bean
    *
    * @param noticeWay 通知方式
    * @param noticeStrategy 策略Bean
    */
    public static void registerBean(String noticeWay, NoticeStrategy noticeStrategy) {
    STRATEGY_MAP.put(noticeWay, noticeStrategy);
    }
    /**
    * 获取策略Bean
    *
    * @param noticeWay 通知方式
    * @return
    */
    public NoticeStrategy getNoticeStrategyBean(String noticeWay) {
    return STRATEGY_MAP.get(noticeWay);
    }

    }
  2. 策略接口

    通知策略接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.liuyanzhao.sens.demo.strategey;
    import com.liuyanzhao.sens.demo.dto.NoticeStrategyDTO;
    /**
    * @author 言曌
    * @date 2020/8/28 12:20 上午
    */
    public interface NoticeStrategy {
    /**
    * 通知用户
    *
    * @param strategyDTO
    */
    void notice(NoticeStrategyDTO strategyDTO);

    }
  3. 策略DTO

    即入参对象封装,里面很重要的属性就是通知方式 noticeWay ,其他字段属于业务字段。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.liuyanzhao.sens.demo.dto;
    import lombok.Data;
    /**
    * 通知策略DTO
    * @author 言曌
    * @date 2020/8/28 12:21 上午
    */
    @Data
    public class NoticeStrategyDTO {
    /**
    * 通知方式: EMAIL、SMS、PHONE、WECHAT
    */
    private String noticeWay;
    /**
    * 用户ID
    */
    private String userId;
    // other

    }
  4. 上下文 Context

    用于提供给调用方直接调用,符合开闭原则。

    即其他代码,如 controller 层需要调用通知用户 notice 方法,直接调用 Context 类的 notice 方法。

    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
    package com.liuyanzhao.sens.demo.context;
    import com.liuyanzhao.sens.demo.dto.NoticeStrategyDTO;
    import com.liuyanzhao.sens.demo.strategey.NoticeStrategy;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    /**
    * @author 言曌
    * @date 2020/8/28 12:40 上午
    */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class NoticeStrategyContext {
    /**
    * 注入策略类:通过构造器方式
    */
    private NoticeStrategy noticeStrategy;
    /**
    * 通知方法
    *
    * @param strategyDTO
    */
    public void notice(NoticeStrategyDTO strategyDTO) {
    noticeStrategy.notice(strategyDTO);
    }

    }
  5. 具体的策略实现

    1)电子邮件通知策略

    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
    package com.liuyanzhao.sens.demo.strategey;
    import com.liuyanzhao.sens.demo.dto.NoticeStrategyDTO;
    import com.liuyanzhao.sens.demo.factory.NoticeStrategyFactory;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.stereotype.Service;
    /**
    * 电子邮件通知策略
    *
    * @author 言曌
    * @date 2020/8/28 12:24 上午
    */
    @Service
    public class EmailNoticeStrategy implements NoticeStrategy, InitializingBean {
    private static final String noticeWay = "EMAIL";
    /**
    * 项目启动时,初始化当前类的Bean时调用
    */
    @Override
    public void afterPropertiesSet() {
    NoticeStrategyFactory.registerBean(noticeWay, this);
    }
    @Override
    public void notice(NoticeStrategyDTO strategyDTO) {
    // TODO 通过电子邮件通知用户具体逻辑
    System.out.println("通过电子邮件通知用户具体逻辑");
    }
    }

    2)微信通知策略

    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
    package com.liuyanzhao.sens.demo.strategey;
    import com.liuyanzhao.sens.demo.dto.NoticeStrategyDTO;
    import com.liuyanzhao.sens.demo.factory.NoticeStrategyFactory;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.stereotype.Service;
    /**
    * 微信通知策略
    *
    * @author 言曌
    * @date 2020/8/28 12:25 上午
    */
    @Service
    public class WechatNoticeStrategy implements NoticeStrategy, InitializingBean {
    private static final String noticeWay = "WECHAT";
    /**
    * 项目启动时,初始化当前类的Bean时调用
    */
    @Override
    public void afterPropertiesSet() {
    NoticeStrategyFactory.registerBean(noticeWay, this);
    }
    @Override
    public void notice(NoticeStrategyDTO strategyDTO) {
    // TODO 通过微信通知用户具体逻辑
    System.out.println("通过微信通知用户具体逻辑");
    }
    }

    其他策略省略,同理,比如以后新增 QQ 通知用户,只需要新增一个 QQNoticeStrategy 类实现通知策略接口即可,然后在这个类里指定具体的 noticeWay 即可。

    这边需要在策略类Bean初始化时,将Bean放到工厂类的 Map ,这样的好处是不需要单独维护 Bean 和 noticeWay 的关系。

Fork me on GitHub