设计模式这东西,基本上属于“看懂一瞬间,用会好几年”。只有实际开发中,当某一模式很好的满足了业务需求时,才会有真切的感觉。借用一句《闪电侠》中,绿箭侠教导闪电侠的台词:“不是你碰巧遇到了它(指闪电事故),而是它选择你”。
业务场景:
航空公司内部对于货运单的价格管理,通常会颁发若干类型的运价文件,典型的有: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的配置,形成一条有规则的"链"即可。