摘要
JDK:1.8.0_202
# 一:场景问题
# 1.1 报价管理
向客户报价,对不同的客户要报不同的价格,比如:
- 对普通客户或者是新客户报的是全价
- 对老客户报的价格,根据客户年限,给予一定的折扣
- 对大客户报的价格,根据大客户的累计消费金额,给予一定的折扣
还要考虑客户购买的数量和金额,比如:虽然是新用户,但是一次购买的数量非常大,或者是总金额非常高,也会有一定的折扣。
还有,报价人员的职务高低,也决定了他是否有权限对价格进行一定的浮动折扣。
甚至在不同的阶段,对客户的报价也不同,一般情况是刚开始比较高,越接近成交阶段,报价越趋于合理。
总之,向客户报价是非常复杂的,因此在一些CRM(客户关系管理)的系统中,会有一个单独的报价管理模块,来处理复杂的报价功能。
为了演示的简洁性,假定现在需要实现一个简化的报价管理,实现如下的功能:
(1)对普通客户或者是新客户报全价
(2)对老客户报的价格,统一折扣5%
(3)对大客户报的价格,统一折扣10%
该怎么实现呢?
# 1.2 不用模式的解决方式
/**
* 价格管理,主要完成计算向客户所报价格的功能
*/
public class Price {
/**
* 报价,对不同类型的,计算不同的价格
*
* @param goodsPrice 商品销售原价
* @param customerType 客户类型
* @return 计算出来的,应该给客户报的价格
*/
public BigDecimal quote(BigDecimal goodsPrice, String customerType) {
switch (customerType) {
case "普通客户":
return this.calcPriceForNormal(goodsPrice);
case "老客户":
return this.calcPriceForOld(goodsPrice);
case "大客户":
return this.calcPriceForLarge(goodsPrice);
}
//其余人员都是报原价
return goodsPrice;
}
/**
* 为新客户或者是普通客户计算应报的价格
*
* @param goodsPrice 商品销售原价
* @return 计算出来的,应该给客户报的价格
*/
private BigDecimal calcPriceForNormal(BigDecimal goodsPrice) {
System.out.println("对于新客户或者是普通客户,没有折扣 ");
return goodsPrice;
}
/**
* 为老客户计算应报的价格
*
* @param goodsPrice 商品销售原价
* @return 计算出来的,应该给客户报的价格
*/
private BigDecimal calcPriceForOld(BigDecimal goodsPrice) {
System.out.println("对于老客户,统一折扣 5%");
return goodsPrice.multiply(BigDecimal.ONE.subtract(new BigDecimal("0.05")));
}
/**
* 为大客户计算应报的价格
*
* @param goodsPrice 商品销售原价
* @return 计算出来的,应该给客户报的价格
*/
private BigDecimal calcPriceForLarge(BigDecimal goodsPrice) {
System.out.println("对于大客户,统一折扣 10%");
return goodsPrice.multiply(BigDecimal.ONE.subtract(new BigDecimal("0.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
55
56
57
58
# 1.3 有何问题
如果存在多个计算方式,这会让这个价格类非常庞大,难以维护。而且,维护和拓展都需要修改已有的代码,这样很不好,违反了 开-闭原则。
其次:经常会有这样的需要,在不同的时候,要使用不同的计算方式
比如:在公司周年庆的时候,所有的客户额外增加3%的折扣;在换季促销的时候,普通客户是额外增加折扣2%,老客户是额外增加折扣3%,大客户是额外增加折扣5%。这意味着计算报价的方式会经常被修改,或者被切换。
通常情况下应该是被切换,因为过了促销时间,又还回到正常的价格体系上来了。而现在的价格类中计算报价的方法,是固定调用各种计算方式,这使得切换调用不同的计算方式很麻烦,每次都需要修改 case 里面的调用代码。
那么到底应该如何实现,才能够让价格类中的计算报价的算法,能很容易的实现可维护、可扩展,又能动态的切换变化呢?
# 二:解决方案
用来解决上述问题的一个合理的解决方案就是策略模式。
策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
# 2.1 解决思路
仔细分析上面的问题,先来把它抽象一下,各种计算报价的计算方式就好比是具体的算法,而使用这些计算方式来计算报价的程序,就相当于是使用算法的客户。
再分析上面的实现方式,为什么会造成那些问题,根本原因,就在于算法和使用算法的客户是耦合的,甚至是密不可分的,在上面实现中,具体的算法和使用算法的客户是同一个类里面的不同方法。
现在要解决那些问题,按照策略模式的方式,应该先把所有的计算方式独立出来,每个计算方式做成一个单独的算法类,从而形成一系列的算法,并且为这一系列算法定义一个公共的接口,这些算法实现是同一接口的不同实现,地位是平等的,可以相互替换。这样一来,要扩展新的算法就变成了增加一个新的算法实现类,要维护某个算法,也只是修改某个具体的算法实现即可,不会对其它代码造成影响。也就是说这样就解决了可维护、可扩展的问题。
为了实现让算法能独立于使用它的客户,策略模式引入了一个上下文的对象,这个对象负责持有算法,但是不负责决定具体选用哪个算法,把选择算法的功能交给了客户,由客户选择好具体的算法后,设置到上下文对象里面,让上下文对象持有客户选择的算法,当客户通知上下文对象执行功能的时候,上下文对象会去转调具体的算法。这样一来,具体的算法和直接使用算法的客户是分离的。
具体的算法和使用它的客户分离过后,使得算法可独立于使用它的客户而变化,并且能够动态的切换需要使用的算法,只要客户端动态的选择使用不同的算法,然后设置到上下文对象中去,实际调用的时候,就可以调用到不同的算法。
# 2.2 模式结构和说明
- Strategy:策略接口,用来约束一系列具体的策略算法。Context使用这个接口来调用具体的策略实现定义的算法。
- ConcreteStrategy:具体的策略实现,也就是具体的算法实现。
- Context:上下文,负责和具体的策略类交互,通常上下文会持有一个真正的策略实现,上下文还可以让具体的策略类来获取上下文的数据,甚至让具体的策略类来回调上下文的方法。
# 2.3 示例代码
定义算法的接口:
/**
* 策略,定义算法的接口
*/
public interface Strategy {
/**
* 某个算法的接口,可以有传入参数,也可以有返回值
*/
void algorithmInterface();
}
2
3
4
5
6
7
8
9
10
11
具体的算法实现了:(ConcreteStrategyA.java、ConcreteStrategyB.java 和 ConcreteStrategyC.java)
/**
* 实现具体的算法
*/
public class ConcreteStrategyA implements Strategy {
@Override
public void algorithmInterface() {
// 具体的算法实现
}
}
2
3
4
5
6
7
8
9
10
/**
* 实现具体的算法
*/
public class ConcreteStrategyB implements Strategy {
@Override
public void algorithmInterface() {
// 具体的算法实现
}
}
2
3
4
5
6
7
8
9
10
/**
* 实现具体的算法
*/
public class ConcreteStrategyC implements Strategy {
@Override
public void algorithmInterface() {
// 具体的算法实现
}
}
2
3
4
5
6
7
8
9
10
# 2.4 重写案例
要使用策略模式来重写前面报价的示例,大致有如下改变:
首先需要定义出算法的接口。
然后把各种报价的计算方式单独出来,形成算法类。
对于Price这个类,把它当做上下文,在计算报价的时候,不再需要判断,直接使用持有的具体算法进行运算即可。选择使用哪一个算法的功能挪出去,放到外部使用的客户端去。
程序结构如果所示:
策略接口:
/**
* 策略,定义计算报价算法的接口
*/
public interface Strategy {
/**
* 计算应报的价格
*
* @param goodsPrice 商品销售原价
* @return 计算出来的,应该给客户报的价格
*/
BigDecimal calcPrice(BigDecimal goodsPrice);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
具体的算法实现:(NormalCustomerStrategy.java、OldCustomerStrategy.java 和 LargeCustomerStrategy.java)
/**
* 具体算法实现,为新客户或者是普通客户计算应报的价格
*/
public class NormalCustomerStrategy implements Strategy {
@Override
public BigDecimal calcPrice(BigDecimal goodsPrice) {
System.out.println("对于新客户或者是普通客户,没有折扣");
return goodsPrice;
}
}
2
3
4
5
6
7
8
9
/**
* 具体算法实现,为老客户计算应报的价格
*/
public class OldCustomerStrategy implements Strategy {
@Override
public BigDecimal calcPrice(BigDecimal goodsPrice) {
System.out.println("对于老客户,统一折扣5%");
return goodsPrice.multiply(BigDecimal.ONE.subtract(new BigDecimal("0.05")));
}
}
2
3
4
5
6
7
8
9
10
/**
* 具体算法实现,为大客户计算应报的价格
*/
public class LargeCustomerStrategy implements Strategy {
@Override
public BigDecimal calcPrice(BigDecimal goodsPrice) {
System.out.println("对于大客户,统一折扣10%");
return goodsPrice.multiply(BigDecimal.ONE.subtract(new BigDecimal("0.1")));
}
}
2
3
4
5
6
7
8
9
10
上下文的实现,原来的价格类,变化比较大,主要有:
原来那些私有的,用来做不同计算的方法,已经去掉了,独立出去做成了算法类
原来报价方法里面,对具体计算方式的判断,去掉了,让客户端来完成选择具体算法的功能
新添加持有一个具体的算法实现,通过构造方法传入
原来报价方法的实现,变化成了转调具体算法来实现
/**
* 价格管理,主要完成计算向客户所报价格的功能
*/
public class Price {
/**
* 持有一个具体的策略对象
*/
private Strategy strategy = null;
/**
* 构造方法,传入一个具体的策略对象
*
* @param strategy 具体的策略对象
*/
public Price(Strategy strategy) {
this.strategy = strategy;
}
/**
* 报价,计算对客户的报价
*
* @param goodsPrice 商品销售原价
* @return 计算出来的,应该给客户报的价格
*/
public BigDecimal quote(BigDecimal goodsPrice) {
return strategy.calcPrice(goodsPrice);
}
}
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
客户端:
public class Client {
public static void main(String[] args) {
//1:选择并创建需要使用的策略对象
Strategy strategy = new LargeCustomerStrategy();
//2:创建上下文
Price ctx = new Price(strategy);
//3:计算报价
BigDecimal quote = ctx.quote(new BigDecimal(1000));
System.out.println("向客户报价:" + quote.doubleValue());
}
}
2
3
4
5
6
7
8
9
10
11
# 三:模式讲解
# 3.1 认识策略模式
1. 策略模式的功能
策略模式的功能是把具体的算法实现,从具体的业务处理里面独立出来,把它们实现成为单独的算法类,从而形成一系列的算法,并让这些算法可以相互替换。
策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、具有更好的维护性和扩展性。
2. 策略模式和 if-else/switch 语句
看了前面的示例,会发现,每个策略算法具体实现的功能,就是原来在 if-else/switch 结构中的具体实现。
其实多个 if-else/switch 语句表达的就是一个平等的功能结构,你要么执行if,要不你就执行else,或者是elseif,这个时候,if块里面的实现和else块里面的实现从运行地位上来讲就是平等的。
而策略模式就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文来与具体的策略类进行交互。
因此多个if-else/switch语句可以考虑使用策略模式。
3. 算法的平等性
策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正是因为这个平等性,才能实现算法之间可以相互替换。
所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。
所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。
4. 谁来选择具体的策略算法
在策略模式中,可以在两个地方来进行具体策略的选择。
一个是在客户端,在使用上下文的时候,由客户端来选择具体的策略算法,然后把这个策略算法设置给上下文。前面的示例就是这种情况。
还有一个是客户端不管,由上下文来选择具体的策略算法,这个在后面讲容错恢复的时候给大家演示一下。
5. Strategy的实现方式
在前面的示例中,Strategy都是使用的接口来定义的,这也是常见的实现方式。但是如果多个算法具有公共功能的话,可以把Strategy实现成为抽象类,然后把多个算法的公共功能实现到Strategy里面。
6. 运行时策略的唯一性
运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态的在不同的策略实现中切换,但是同时只能使用一个。
# 3.2 优缺点
1. 定义一系列算法
策略模式的功能就是定义一系列算法,实现让这些算法可以相互替换。所以会为这一系列算法定义公共的接口,以约束一系列算法要实现的功能。如果这一系列算法具有公共功能,可以把策略接口实现成为抽象类,把这些公共功能实现到父类里面,对于这个问题,前面讲了三种处理方法,这里就不罗嗦了。
2. 避免多重条件语句
根据前面的示例会发现,策略模式的一系列策略算法是平等的,可以互换的,写在一起就是通过 if-else/switch 结构来组织,如果此时具体的算法实现里面又有条件语句,就构成了多重条件语句,使用策略模式能避免这样的多重条件语句。
3. 更好的扩展性
在策略模式中扩展新的策略实现非常容易,只要增加新的策略实现类,然后在选择使用策略的地方选择使用这个新的策略实现就好了。
4. 客户必须了解每种策略的不同
策略模式也有缺点,比如让客户端来选择具体使用哪一个策略,这就可能会让客户需要了解所有的策略,还要了解各种策略的功能和不同,这样才能做出正确的选择,而且这样也暴露了策略的具体实现。
5. 增加了对象数目
由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。
6. 只适合扁平的算法结构
策略模式的一系列算法地位是平等的,是可以相互替换的,事实上构成了一个扁平的算法结构,也就是在一个策略接口下,有多个平等的策略算法,就相当于兄弟算法。而且在运行时刻只有一个算法被使用,这就限制了算法使用的层级,使用的时候不能嵌套使用。
对于出现需要嵌套使用多个算法的情况,比如折上折、折后返卷等业务的实现,需要组合或者是 嵌套使用多个算法的情况,可以考虑使用装饰模式、或是变形的职责链、或是AOP等方式来实现。
# 3.3 思考策略模式
1. 策略模式的本质
策略模式的本质:分离算法,选择实现
仔细思考策略模式的结构和实现的功能,会发现,如果没有上下文,策略模式就回到了最基本的接口和实现了,只要是面向接口编程的,那么就能够享受到接口的封装隔离带来的好处。也就是通过一个统一的策略接口来封装和隔离具体的策略算法,面向接口编程的话,自然不需要关心具体的策略实现,也可以通过使用不同的实现类来实例化接口,从而实现切换具体的策略。
看起来好像没有上下文什么事情,但是如果没有上下文,那么就需要客户端来直接与具体的策略交互,尤其是当需要提供一些公共功能,或者是相关状态存储的时候,会大大增加客户端使用的难度。因此,引入上下文还是很必要的,有了上下文,这些工作就由上下文来完成了,客户端只需要与上下文交互就可以了,这样会让整个设计模式更独立、更有整体性,也让客户端更简单。
但纵观整个策略模式实现的功能和设计,它的本质还是“分离算法,选择实现”,因为分离并封装了算法,才能够很容易的修改和添加算法;也能很容易的动态切换使用不同的算法,也就是动态选择一个算法来实现需要的功能了。
2. 对设计原则的体现
从设计原则上来看,策略模式很好的体现了开-闭原则。策略模式通过把一系列可变的算法进行封装,并定义出合理的使用结构,使得在系统出现新算法的时候,能很容易的把新的算法加入到已有的系统中,而已有的实现不需要做任何修改。这在前面的示例中已经体现出来了,好好体会一下。
从设计原则上来看,策略模式还很好的体现了里氏替换原则。策略模式是一个扁平结构,一系列的实现算法其实是兄弟关系,都是实现同一个接口或者继承的同一个父类。这样只要使用策略的客户保持面向抽象类型编程,就能够使用不同的策略的具体实现对象来配置它,从而实现一系列算法可以相互替换。
3. 何时选用策略模式
建议在如下情况中,选用策略模式:
出现有许多相关的类,仅仅是行为有差别的情况,可以使用策略模式来使用多个行为中的一个来配置一个类的方法,实现算法动态切换。
出现同一个算法,有很多不同的实现的情况,可以使用策略模式来把这些“不同的实现”实现成为一个算法的类层次。
需要封装算法中,与算法相关的数据的情况,可以使用策略模式来避免暴露这些跟算法相关的数据结构。
出现抽象一个定义了很多行为的类,并且是通过多个 if-else/switch 语句来选择这些行为的情况,可以使用策略模式来代替这些条件语句。
# 3.4 相关模式
1. 策略模式和状态模式
这两个模式从模式结构上看是一样的,但是实现的功能是不一样的。
状态模式是根据状态的变化来选择相应的行为,不同的状态对应不同的类,每个状态对应的类实现了该状态对应的功能,在实现功能的同时,还会维护状态数据的变化。这些实现状态对应的功能的类之间是不能相互替换的。
策略模式是根据需要或者是客户端的要求来选择相应的实现类,各个实现类是平等的,是可以相互替换的。
另外策略模式可以让客户端来选择需要使用的策略算法,而状态模式一般是由上下文,或者是在状态实现类里面来维护具体的状态数据,通常不由客户端来指定状态。
2. 策略模式和模板方法模式
这两个模式可组合使用,如同前面示例的那样。
模板方法重在封装算法骨架,而策略模式重在分离并封装算法实现。
3. 策略模式和享元模式
这两个模式可组合使用。
策略模式分离并封装出一系列的策略算法对象,这些对象的功能通常都比较单一,很多时候就是为了实现某个算法的功能而存在,因此,针对这一系列的、多个细粒度的对象,可以应用享元模式来节省资源,但前提是这些算法对象要被频繁的使用,如果偶尔用一次,就没有必要做成享元了。
# 四:JDK
- java.util.Comparator#compare() (opens new window)
- javax.servlet.http.HttpServlet (opens new window)
- javax.servlet.Filter#doFilter() (opens new window)