行为型设计模式

行为型模式主要处理类或对象如何交互及如何分配职责。它共有 11 种模式,策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式和解释器模式。

策略模式

策略模式能够解决当写代码时总会遇到的很多的 if…else 或者 case。过多的条件语句或者多重语句嵌套会使代码变得臃肿,维护的成本也会加大,这显然违背了开放封闭原则。

定义:定义一系列的算法,把每一个算法封装起来,并且使它们可相互替换。策略模式使得算法可独立于使用它的客户而独立变化。

在策略模式中有如下角色:

  • Context:上下文角色,用来操作策略的上下文环境,起到承上启下的作用,屏蔽高层模块对策略、算法的直接访问。
  • Stragety:抽象策略角色,策略、算法的抽象,通常为接口。
  • ConcreteStragety:具体的策略实现。

策略模式的简单实现

假设张无忌制定了 3 种应战的策略,分别对付 3 个实力层次的对手。

定义策略接口:

1
2
3
4
5
6
/**
* 策略接口有一个方法用于战斗
*/
public interface FightingStrategy{
void fighting();
}

具体策略实现:

分别定义了 3 个策略来实现策略接口,用来对付 3 个实力层次的对手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class WeakRivalStrategy implements FightingStrategy{

@Override
public void fighting() {
System.out.println("较弱对手");
}
}
public class CommonRivalStrategy implements FightingStrategy{

@Override
public void fighting() {
System.out.println("普通对手");
}
}
public class StrongRivalStrategy implements FightingStrategy{

@Override
public void fighting() {
System.out.println("强大对手");
}
}

上下文角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 上下文角色的构造方法包含了策略类,通过传进来不同的具体策略来调用不同策略的 fighting 方法。
*/
public class Context{
private FightingStrategy fightingStrategy;

public Context(FightingStrategy fightingStrategy) {
this.fightingStrategy = fightingStrategy;
}

public void fighting(){
fightingStrategy.fighting();
}
}

客户端调用:

1
2
3
4
5
6
7
8
9
10
11
12
public class ZhangWuJi{
public static void main(String[] args) {
Context context;
// 面对不同层次的对手,采取不同的策略。这里省略了对不同实力层次的判断。
context = new Context(new WeakRivalStrategy());
context.fighting();
context = new Context(new CommonRivalStrategy());
context.fighting();
context = new Context(new StrongRivalStrategy());
context.fighting();
}
}

使用场景和优缺点

  • 使用场景
    • 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。
    • 针对同一类型问题的多种处理方式,仅仅是具体行为有差别时。
    • 在一个类中定义了很多行为,而且这些行为在这个类里的操作以多个条件语句的形式出现。策略模式将相关的条件分支移入它们各自的 Strategy 类中,以代替这些条件语句。
  • 优点
    • 使用策略模式可以避免使用多重条件语句。多重条件语句不易维护,而且易出错。
    • 易于拓展。当需要添加一个策略时,只需要实现接口就可以了。
  • 缺点
    • 每一个策略都是一个类,复用性小。如果策略过多,类的数量会增多。
    • 上层模块必须知道有哪些策略,才能够使用这些策略,这与迪米特原则相违背。

模板方法模式

在软件开发中,有时会遇到类似的情况;某个方法的实现需要多个步骤,其中有些步骤是固定的,而有些步骤并不固定,存在可变性。为了提高代码的复用性和系统的灵活性,可以使用模板方法模式来应对这类情况。

定义:定义一个操作中的算法框架,而将一些步骤延迟到子类中,使得子类不改变一个算法的结构即可重定义算法的某些特定步骤。

在模板方法中有如下角色:

  • AbstractClass:抽象类,定义了一套算法框架。
  • ConcreteClass:具体实现类。

模板方法的简单实现:

模板方法实际就是封装固定的流程,像模板一样,第一步做什么,第二步又做什么,都在抽象类中定义好。子类可以有不同的算法实现,在算法框架不被修改的前提下实现某些步骤的算法替换。

创建抽象类,定义算法框架:

一个武侠要战斗时,也有一套固定的通用模式,那就是运行内功、开启经脉、准备武器和使用招式。

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
/**
* 此抽象类包含了 3 种类型的方法:抽象方法、具体方法和钩子方法。
* 1. 抽象方法是交由子类去实现的
* 2. 具体方法则是父类实现了子类公共的方法。(武侠开启经脉的方式都一样,所以就在具体方法中实现。)
* 3. 钩子方法分为两类:
* 第一类如 hook 方法,是一个空实现的方法,子类可以视情况来决定是否要覆盖它。
* 第二类如 hasWeapons 方法,这类钩子方法的返回类型通常是 boolean 类型的,
* 其一般用于对某个条件进行判断,如果条件满足则执行某一步骤,否则将不执行。
*/
public abstract class AbstractSwordsman{
// 该方法为 final,防止算法框架被覆写。
public final void fighting(){
// 运行内功,抽象方法。
neigong();
// 调整经脉,具体方法。
meridian();
// 如果有武器,则准备武器。
if (hasWeapons()){
weapons();
}
// 使用招式
moves();
// 钩子方法
hook();
}

// 空实现方法
protected void hook() {}

protected abstract void moves();
protected abstract void weapons();
protected abstract void neigong();
protected void meridian(){
System.out.println("开启经脉");
}

/**
* 是否有武器,默认有,钩子方法。
* @return
*/
protected boolean hasWeapons() {
return true;
}
}

具体实现类:

以武侠张无忌和张三丰为例。

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
public class ZhangWuJi extends AbstractSwordsman{

@Override
protected void moves() {
System.out.println("使用招式");
}

@Override
protected void weapons() {

}

@Override
protected void neigong() {
System.out.println("运行内功");
}

/**
* 没有武器,返回 false,这样也不会进入 weapons 方法了。
* @return
*/
@Override
protected boolean hasWeapons() {
return false;
}
}


public class ZhangSanFeng extends AbstractSwordsman{

@Override
protected void moves() {
System.out.println("使用招式");
}

@Override
protected void weapons() {
System.out.println("使用武器");
}

@Override
protected void neigong() {
System.out.println("运行内功");
}

@Override
protected void hook() {
System.out.println("处理自定义逻辑");
}
}

客户端调用:

1
2
3
4
5
6
7
8
public class Client{
public static void main(String[] args) {
ZhangWuJi zhangWuJi = new ZhangWuJi();
zhangWuJi.fighting();
ZhangSanFeng zhangSanFeng = new ZhangSanFeng();
zhangSanFeng.fighting();
}
}

使用场景和优缺点

  • 使用场景
    • 多个子类有共有的方法,并且逻辑基本相同时。
    • 面对重要、复杂的算法,可以把核心算法设计为模板方法,周边相关细节功能则由各个子类实现。
    • 需要通过子类来决定父类算法中的某个步骤是否执行,实现子类对父类的反向控制。
  • 优点
    • 模板方法模式通过把不变的行为搬移到超类,去除了子类中的重复代码。
    • 子类实现算法的某些细节,有助于算法的拓展。
  • 缺点
    • 每个不同的实现都需要定义一个子类,这会导致类的个数的增加,设计更加抽象。

观察者模式(发布-订阅模式)

定义:定义对象间一种一对多的依赖关系,每当一个对象改变状态时,则所有依赖于它的对象都会得到通知并被自动更新。

在观察者模式中有如下角色:

  • Subject:抽象主题(抽象被观察者)。抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。
  • ConcreteSubject:具体主题(具体被观察者)。该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
  • Observer:抽象观察者,是观察者的抽象类。它定义了一个更新接口,使得在得到主题更改通知时更新自己。
  • ConcreteObserver:具体观察者。实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

观察者模式的简单实现

以微信公众号为例,假设用户是观察者,公众号是被观察者,当某个公众号更新时就会通知这些订阅的微信用户。

抽象观察者:

1
2
3
4
5
6
/**
* 定义了一个更新的方法
*/
public interface Observer{
public void update(String message);
}

具体观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 微信用户是观察者,里面实现了更新的方法。
*/
public class WeixinUser implements Observer{

private String name;

public WeixinUser(String name) {
this.name = name;
}

@Override
public void update(String message) {
System.out.println(name+" - "+ message);
}
}

抽象被观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Subject {
/**
* 增加订阅者
*/
void attach(Observer observer);

/**
* 删除订阅者
*/
void detach(Observer observer);

/**
* 通知订阅者更新消息
*/
void notify(String string);
}

具体被观察者

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
/**
* 微信公众号是具体主题,里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法。
*/
public class SubscriptionSubject implements Subject{

/**
* 存储订阅公众号的微信用户
*/
private List<Observer> weixinUserList = new ArrayList<>();

@Override
public void attach(Observer observer) {
weixinUserList.add(observer);
}

@Override
public void detach(Observer observer) {
weixinUserList.remove(observer);
}

@Override
public void notify(String message) {
for (Observer observer : weixinUserList){
observer.update(message);
}
}
}

客户端调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client{
public static void main(String[] args) {
SubscriptionSubject subscriptionSubject = new SubscriptionSubject();
// 创建微信用户
WeixinUser weixinUser1= new WeixinUser("user1");
WeixinUser weixinUser2= new WeixinUser("user2");
WeixinUser weixinUser3= new WeixinUser("user3");
// 订阅公众号
subscriptionSubject.attach(weixinUser1);
subscriptionSubject.attach(weixinUser2);
subscriptionSubject.attach(weixinUser3);
// 公众号更新发出消息给订阅的微信用户
subscriptionSubject.notify("更新消息");
}
}

使用场景和优缺点

  • 使用场景
    • 关联行为场景。需要注意的是,关联行为是可拆分的,而不是 ”组合“ 关系。
    • 事件多级触发场景。
    • 跨系统的消息交换场景,如消息队列、事件总线的处理机制。
  • 优点
    • 观察者和被观察者之间是抽象耦合,容易扩展。
    • 方便建立一套触发机制。
  • 缺点
    • 在应用观察者模式时需要考虑一下开发效率和运行效率的问题。程序中包括一个被观察者、多个观察者,开发、调试等内容会比较复杂,而且在 Java 中消息的通知一般是顺序执行的,那么一个观察者卡顿,会影响整体的执行效率,在这种情况下,一般会采用异步方式。

备注

参考资料:

Android 进阶之光