设计模式之策略

什么是策略模式?

定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换,这就叫策略模式 strategy。 比如你打算去旅行(目标),你可以选 “坐飞机”、“开私驾车” 或 “骑自行车”(不同策略)。这些方式随你挑,但不影响你 “去旅行” 这个最终目的。


简单示例

商场有多种促销手段:原价、打 8 折、满 300 减 50。使用策略模式实现一个商场打折计算器。


第一步:定义策略接口 (Strategy)

1
2
3
public interface DiscountStrategy {
double calculate(double price); // 统一的计算接口
}

第二步:实现具体的策略类 (ConcreteStrategy)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 策略 A:不打折
class NoDiscount implements DiscountStrategy {
public double calculate(double price) { return price; }
}

// 策略 B:打 8 折
class PercentDiscount implements DiscountStrategy {
public double calculate(double price) { return price * 0.8; }
}

// 策略 C:满减
class FullReductionDiscount implements DiscountStrategy {
public double calculate(double price) { return price >= 300 ? price - 50 : price; }
}

第三步:定义上下文 (Context)

1
2
3
4
5
6
7
8
9
10
11
public class Cashier {
private DiscountStrategy strategy;

public void setStrategy(DiscountStrategy strategy) {
this.strategy = strategy;
}

public double quote(double price) {
return strategy.calculate(price);
}
}

第四步:客户端调用

1
2
3
4
5
6
7
8
9
public class Client {
public static void main(String[] args) {
Cashier cashier = new Cashier();

// 今天过节,全场 8 折
cashier.setStrategy(new PercentDiscount());
System.out.println("应付金额:" + cashier.quote(500)); // 400.0
}
}


Shiro 如何通过策略模式处理多个 Realm?

在 Shiro 中,当你配置了多个数据源(比如一个从 LDAP 查,一个从数据库查),ModularRealmAuthenticator(认证器)就会面临一个问题:“只要一个对就行,还是全都要对?”

这就是典型的策略模式应用。Shiro 定义了一个接口:AuthenticationStrategy。Shiro 提供的三种内置策略:

  • FirstSuccessfulStrategy:只要第一个 Realm 验证成功,就算成功,后面的不看了。

  • AtLeastOneSuccessfulStrategy(默认):只要有一个 Realm 成功就行,但它会尝试完所有的 Realm。

  • AllSuccessfulStrategy:必须所有的 Realm 都认证成功才行。


核心源码实现

在 ModularRealmAuthenticator 的认证逻辑中,它会循环遍历所有的 Realm,并在循环前后调用策略的方法:

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
// 伪代码展示 ModularRealmAuthenticator 的核心逻辑
protected AuthenticationInfo doAuthenticate(AuthenticationToken token) {
// 1. 获取所有的策略(策略模式的体现)
AuthenticationStrategy strategy = getAuthenticationStrategy();

// 2. 准备一个汇总的结果集
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);

for (Realm realm : realms) {
// 询问策略:在尝试这个 Realm 之前要做什么吗?
aggregate = strategy.beforeAttempt(realm, token, aggregate);

try {
AuthenticationInfo info = realm.getAuthenticationInfo(token);
// 询问策略:这个 Realm 成功了,现在结果集怎么合并?
aggregate = strategy.afterAttempt(realm, token, info, aggregate, null);
} catch (Throwable t) {
// 询问策略:这个 Realm 失败了,咱们还继续吗?
aggregate = strategy.afterAttempt(realm, token, null, aggregate, t);
}
}

// 3. 所有 Realm 跑完了,询问策略:最后给个话,这人到底算不算登录成功?
return strategy.afterAllAttempts(token, aggregate);
}

经过这样设计,认证器(Authenticator)只负责“跑循环”,至于“怎么算成功”,全部外包给“策略对象”。如果你公司有奇葩需求(比如:必须两个特定的数据库都通过才行),你只需要写一个类实现 AuthenticationStrategy 接口并注入进去,而不需要修改 Shiro 的核心源码。