设计模式这东西,基本上属于“看懂一瞬间,用会好几年”。只有实际开发中,当某一模式很好的满足了业务需求时,才会有真切的感觉。借用一句《闪电侠》中,绿箭侠教导闪电侠的台词:“不是你碰巧遇到了它(指闪电事故),而是它选择你”。
业务场景:
航空公司内部对于货运单的价格管理,通常会颁发若干类型的运价文件,典型的有:SpotRate(一票一议)、ContractRate(合同运价)、PublicRate(IATA公布运价)等等,一票运单判断该用何种运价时,通常会按一定的顺序在这几类运价中依次匹配查找,如果匹配成功,则直接返回,使用查找结果中的费率做为计算依据。
变化点:
不同的航空公司,内部管理体制不同,支持的运价种类也不同,包括查找运价的顺序也可能略有差异。
目标:
为了能尽量少加班,少改代码,要求系统最好能方便的应对这些变化。职责链模式正是为该类场景而生,园友飞林沙已经详解解读了这一模式,参见其博文:
重温设计模式(三)——职责链模式(chain of responsibility)
类图:
RateCluase 为运价条款基本信息
Airwaybill 为运单基本信息
这二个类的实例,主要做为查找运价的入口参数
RateFinder为统一接口,find方法为查找运价,nextFinder的setter/getter用于指定下一个查找者
XXXRateFinder为具体的实现类,为了简化问题,这里只列了3种基本的实现(实际情况远比这复杂)
代码:
入口参数
1 /***********************************************************************
2 * Module: AirwayBill.java
3 * Author: jimmy
4 * Purpose: Defines the Class AirwayBill
5 ***********************************************************************/
6
7 package murate.test.ratefinder.dto;
8
9 public class AirwayBill {
10 /**
11 * 运单前缀
12 *
13 */
14 private String awbPre;
15 /**
16 * 运单号
17 *
18 */
19 private String awbNo;
20 /**
21 * 始发站
22 *
23 */
24 private String origin;
25 /**
26 * 目的站
27 *
28 */
29 private String dest;
30 /**
31 * 代理人帐号
32 *
33 */
34 private String agentNumber;
35 /**
36 * 品名代码
37 *
38 */
39 private String commodityCode;
40 /**
41 * 特货代码
42 *
43 */
44 private String specialHandlingCode;
45
46 public String getAwbPre() {
47 return awbPre;
48 }
49
50 public void setAwbPre(String awbPre) {
51 this.awbPre = awbPre;
52 }
53
54 public String getAwbNo() {
55 return awbNo;
56 }
57
58 public void setAwbNo(String awbNo) {
59 this.awbNo = awbNo;
60 }
61
62 public String getOrigin() {
63 return origin;
64 }
65
66 public void setOrigin(String origin) {
67 this.origin = origin;
68 }
69
70 public String getDest() {
71 return dest;
72 }
73
74 public void setDest(String dest) {
75 this.dest = dest;
76 }
77
78 public String getAgentNumber() {
79 return agentNumber;
80 }
81
82 public void setAgentNumber(String agentNumber) {
83 this.agentNumber = agentNumber;
84 }
85
86 public String getCommodityCode() {
87 return commodityCode;
88 }
89
90 public void setCommodityCode(String commodityCode) {
91 this.commodityCode = commodityCode;
92 }
93
94 public String getSpecialHandlingCode() {
95 return specialHandlingCode;
96 }
97
98 public void setSpecialHandlingCode(String specialHandlingCode) {
99 this.specialHandlingCode = specialHandlingCode;
100 }
101
102 }
1 /***********************************************************************
2 * Module: RateCluase.java
3 * Author: jimmy
4 * Purpose: Defines the Class RateCluase
5 ***********************************************************************/
6
7 package murate.test.ratefinder.dto;
8
9 /**
10 * 运价条款
11 *
12 * 2014-12-24 杨俊明 0.1
13 *
14 */
15 public class RateCluase {
16
17 /**
18 * 条款Id
19 *
20 */
21 private Long clauseId;
22
23 /**
24 * 条款名称
25 *
26 */
27 private String clauseName;
28
29 /**
30 * 运单前缀
31 */
32 private String awbPre;
33
34 /**
35 * 运单号
36 */
37 private String awbNo;
38
39 /**
40 * 始发站
41 *
42 */
43 private String origin;
44
45 /**
46 * 目的站
47 *
48 */
49 private String dest;
50
51 /**
52 * 代理人帐号
53 *
54 */
55 private String agentNumber;
56
57 /**
58 * 品名代码
59 *
60 */
61 private String commodityCode;
62
63 /**
64 * 特货代码
65 *
66 */
67 private String specialHandlingCode;
68
69 public Long getClauseId() {
70 return clauseId;
71 }
72
73 public void setClauseId(Long clauseId) {
74 this.clauseId = clauseId;
75 }
76
77 public String getClauseName() {
78 return clauseName;
79 }
80
81 public void setClauseName(String clauseName) {
82 this.clauseName = clauseName;
83 }
84
85 public String getOrigin() {
86 return origin;
87 }
88
89 public void setOrigin(String origin) {
90 this.origin = origin;
91 }
92
93 public String getDest() {
94 return dest;
95 }
96
97 public void setDest(String dest) {
98 this.dest = dest;
99 }
100
101 public String getAgentNumber() {
102 return agentNumber;
103 }
104
105 public void setAgentNumber(String agentNumber) {
106 this.agentNumber = agentNumber;
107 }
108
109 public String getCommodityCode() {
110 return commodityCode;
111 }
112
113 public void setCommodityCode(String commodityCode) {
114 this.commodityCode = commodityCode;
115 }
116
117 public String getSpecialHandlingCode() {
118 return specialHandlingCode;
119 }
120
121 public void setSpecialHandlingCode(String specialHandlingCode) {
122 this.specialHandlingCode = specialHandlingCode;
123 }
124
125 public String getAwbPre() {
126 return awbPre;
127 }
128
129 public void setAwbPre(String awbPre) {
130 this.awbPre = awbPre;
131 }
132
133 public String getAwbNo() {
134 return awbNo;
135 }
136
137 public void setAwbNo(String awbNo) {
138 this.awbNo = awbNo;
139 }
140
141 public String toString() {
142 return clauseName;
143 }
144
145 }
接口:
1 /***********************************************************************
2 * Module: RateFinder.java
3 * Author: jimmy
4 * Purpose: Defines the Interface RateFinder
5 ***********************************************************************/
6
7 package murate.test.ratefinder.service;
8
9 import java.util.List;
10
11 import murate.test.ratefinder.dto.AirwayBill;
12 import murate.test.ratefinder.dto.RateCluase;
13
14 /**
15 * 运价查找接口
16 *
17 */
18 public interface RateFinder {
19 /**
20 * 查找运价条款
21 *
22 * @param airwayBill
23 * 运单信息
24 * @param rateClauses
25 * 运单条款信息
26 * @return
27 */
28 RateCluase find(AirwayBill airwayBill, List<RateCluase> rateClauses);
29
30 RateFinder getNextFinder();
31
32 void setNextFinder(RateFinder value);
33
34 }
3个实现类:
1 /***********************************************************************
2 * Module: SpotRateFinder.java
3 * Author: jimmy
4 * Purpose: Defines the Class SpotRateFinder
5 ***********************************************************************/
6
7 package murate.test.ratefinder.service.impl;
8
9 import java.util.*;
10
11 import org.springframework.util.StringUtils;
12
13 import murate.test.ratefinder.dto.AirwayBill;
14 import murate.test.ratefinder.dto.RateCluase;
15 import murate.test.ratefinder.service.RateFinder;
16
17 /**
18 * 一票一议运价查找
19 *
20 */
21 public class SpotRateFinder implements RateFinder {
22
23 RateFinder nextFinder;
24
25 public RateCluase find(AirwayBill airwayBill, List<RateCluase> rateClauses) {
26
27 for (RateCluase clause : rateClauses) {
28 // 模拟查找逻辑(只要单号匹配成功,就算通过,仅演示)
29
30 if (StringUtils.isEmpty(clause.getAwbPre())
31 || StringUtils.isEmpty(clause.getAwbNo())
32 || StringUtils.isEmpty(airwayBill.getAwbPre())
33 || StringUtils.isEmpty(airwayBill.getAwbNo())) {
34 continue;
35 }
36 if (clause.getAwbPre().equals(airwayBill.getAwbPre())
37 && clause.getAwbNo().equals(airwayBill.getAwbNo())) {
38 // 找到了,直接返回
39 return clause;
40 }
41 }
42
43 // 否则,交给下一个Finder继续查找
44 return nextFinder.find(airwayBill, rateClauses);
45
46 }
47
48 public RateFinder getNextFinder() {
49 return nextFinder;
50 }
51
52 public void setNextFinder(RateFinder value) {
53 nextFinder = value;
54 }
55
56 }
1 /***********************************************************************
2 * Module: ContractRateFinder.java
3 * Author: jimmy
4 * Purpose: Defines the Class ContractRateFinder
5 ***********************************************************************/
6
7 package murate.test.ratefinder.service.impl;
8
9 import java.util.*;
10
11 import org.springframework.util.StringUtils;
12
13 import murate.test.ratefinder.dto.AirwayBill;
14 import murate.test.ratefinder.dto.RateCluase;
15 import murate.test.ratefinder.service.RateFinder;
16
17 /**
18 * Contract运价查找者
19 *
20 */
21 public class ContractRateFinder implements RateFinder {
22 RateFinder nextFinder;
23
24 public RateCluase find(AirwayBill airwayBill, List<RateCluase> rateClauses) {
25
26 for (RateCluase clause : rateClauses) {
27
28 // 模拟查找逻辑(只要代理人帐号匹配成功,就算通过,仅演示)
29
30 if (StringUtils.isEmpty(clause.getAgentNumber())
31 || StringUtils.isEmpty(clause.getAgentNumber())) {
32 continue;
33 }
34
35 if (clause.getAgentNumber().equals(airwayBill.getAgentNumber())) {
36 // 找到了,直接返回
37 return clause;
38 }
39 }
40
41 // 否则,交给下一个Finder继续查找
42 return nextFinder.find(airwayBill, rateClauses);
43
44 }
45
46 public RateFinder getNextFinder() {
47 return nextFinder;
48 }
49
50 public void setNextFinder(RateFinder value) {
51 nextFinder = value;
52 }
53
54 }
1 /***********************************************************************
2 * Module: PublicRateFinder.java
3 * Author: jimmy
4 * Purpose: Defines the Class PublicRateFinder
5 ***********************************************************************/
6 package murate.test.ratefinder.service.impl;
7
8 import java.util.*;
9
10 import org.springframework.util.StringUtils;
11
12 import murate.test.ratefinder.dto.AirwayBill;
13 import murate.test.ratefinder.dto.RateCluase;
14 import murate.test.ratefinder.service.RateFinder;
15
16 /**
17 * 公布运价查找者
18 *
19 */
20 public class PublicRateFinder implements RateFinder {
21 RateFinder nextFinder;
22
23 public RateCluase find(AirwayBill airwayBill, List<RateCluase> rateClauses) {
24
25 for (RateCluase clause : rateClauses) {
26 // 模拟查找逻辑(只要始发站、目的站匹配,就算通过,仅演示)
27
28 if (StringUtils.isEmpty(clause.getOrigin())
29 || StringUtils.isEmpty(clause.getDest())
30 || StringUtils.isEmpty(airwayBill.getOrigin())
31 || StringUtils.isEmpty(airwayBill.getDest())) {
32 continue;
33 }
34
35 if (clause.getOrigin().equals(airwayBill.getOrigin())
36 && clause.getDest().equals(airwayBill.getDest())) {
37 // 找到了,直接返回
38 return clause;
39 }
40 }
41
42 if (nextFinder == null) {
43 return null;
44 }
45
46 // 否则,交给下一个Finder继续查找
47 return nextFinder.find(airwayBill, rateClauses);
48
49 }
50
51 public RateFinder getNextFinder() {
52 return nextFinder;
53 }
54
55 public void setNextFinder(RateFinder value) {
56 nextFinder = value;
57 }
58 }
注:链的最后一个节点,要有保底处理,即 PublicRateFinder 类42-44 行的处理,否则到“链”的最后一个节点,就会出错了。
配置:
该万能的Spring出场了:
1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
4 xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
5 xmlns:context="http://www.springframework.org/schema/context"
6 xsi:schemaLocation="
7 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
8 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
9 http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
10 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
11 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
12 default-autowire="byName">
13
14 <!-- spotrate->contract->public -->
15
16 <!-- <bean id="firstFinder" class="murate.test.ratefinder.service.impl.SpotRateFinder">
17 <property name="nextFinder" ref="contractRateFinder" />
18 </bean>
19
20 <bean id="contractRateFinder" class="murate.test.ratefinder.service.impl.ContractRateFinder">
21 <property name="nextFinder" ref="publicRateFinder" />
22 </bean>
23
24 <bean id="publicRateFinder" class="murate.test.ratefinder.service.impl.PublicRateFinder"></bean>
25 -->
26
27 <!-- contract->spotrate->public -->
28
29 <bean id="firstFinder" class="murate.test.ratefinder.service.impl.ContractRateFinder">
30 <property name="nextFinder" ref="spotRateFinder" />
31 </bean>
32
33 <bean id="spotRateFinder" class="murate.test.ratefinder.service.impl.SpotRateFinder">
34 <property name="nextFinder" ref="publicRateFinder" />
35 </bean>
36
37 <bean id="publicRateFinder" class="murate.test.ratefinder.service.impl.PublicRateFinder"></bean>
38
39
40 </beans>
测试代码:
1 package murate.test;
2
3 import java.util.ArrayList;
4 import java.util.List;
5
6 import murate.test.ratefinder.dto.AirwayBill;
7 import murate.test.ratefinder.dto.RateCluase;
8 import murate.test.ratefinder.service.RateFinder;
9
10 import org.junit.Test;
11 import org.springframework.context.ApplicationContext;
12 import org.springframework.context.support.ClassPathXmlApplicationContext;
13
14 public class RateFinderTest {
15
16 @Test
17 public void testFinder() {
18
19 ApplicationContext ctx = new ClassPathXmlApplicationContext(
20 "spring-beans-test.xml");
21
22 RateFinder firstFinder = ctx.getBean("firstFinder", RateFinder.class);
23
24 List<AirwayBill> awbs = getAwbList();
25 List<RateCluase> rateCluases = getRateClauses();
26
27 for (AirwayBill airwayBill : awbs) {
28 System.out.println(airwayBill.getAwbPre() + airwayBill.getAwbNo()
29 + ":" + firstFinder.find(airwayBill, rateCluases));
30 }
31
32 ((ClassPathXmlApplicationContext) ctx).close();
33 }
34
35 /**
36 * 模拟所有运价条款
37 * @return
38 */
39 private List<RateCluase> getRateClauses() {
40 List<RateCluase> rateCluases = new ArrayList<RateCluase>();
41
42 RateCluase spa = new RateCluase();
43 spa.setAwbPre("112");
44 spa.setAwbNo("00000000");
45 spa.setClauseName("SpotRate测试条款");
46 rateCluases.add(spa);
47
48 RateCluase contract = new RateCluase();
49 contract.setAgentNumber("SHAXYZ");
50 contract.setClauseName("Contract测试条款 ");
51 rateCluases.add(contract);
52
53 RateCluase publicClause = new RateCluase();
54 publicClause.setOrigin("PVG");
55 publicClause.setDest("LAX");
56 publicClause.setClauseName("Public测试条款 ");
57 rateCluases.add(publicClause);
58
59 return rateCluases;
60
61 }
62
63 /**
64 * 模拟生成运单数据
65 * @return
66 */
67 private List<AirwayBill> getAwbList() {
68
69 //awb1预期匹配Contract条款(或SpotRate,视配置规定的查找顺序)
70 AirwayBill awb1 = new AirwayBill();
71 awb1.setAgentNumber("SHAXYZ");
72 awb1.setAwbPre("112");
73 awb1.setAwbNo("00000000");
74
75 //awb2预期匹配Public条款
76 AirwayBill awb2 = new AirwayBill();
77 awb2.setOrigin("PVG");
78 awb2.setDest("LAX");
79 awb2.setAwbPre("112");
80 awb2.setAwbNo("11111111");
81
82 //awb3预期匹配SpotRate条款
83 AirwayBill awb3 = new AirwayBill();
84 awb3.setAwbPre("112");
85 awb3.setAwbNo("22222222");
86
87 List<AirwayBill> awbList = new ArrayList<AirwayBill>();
88 awbList.add(awb1);
89 awbList.add(awb2);
90 awbList.add(awb3);
91
92 return awbList;
93
94 }
95 }
运行结果:
11200000000:Contract测试条款 11211111111:Public测试条款 11222222222:null
如果把配置中,注释部分和未注释部分对换,即:更改查找顺序,则变成了
11200000000:SpotRate测试条款 11211111111:Public测试条款 11222222222:null
业务扩展:如果以后某航空公司又发明了一种新运价,增加RateFinder的实现类,然后在配置中,把新的处理类,挂到链中的适当位置即可。反之,如果某一类运价,不再使用了,还是修改配置,把这个节点从链中摘除。至于查找顺序的修改,通过nextFinder的配置,形成一条有规则的"链"即可。