設計模式這東西,基本上屬于“看懂一瞬間,用會好幾年”。只有實際開發中,當某一模式很好的滿足了業務需求時,才會有真切的感覺。借用一句《閃電俠》中,綠箭俠教導閃電俠的臺詞:“不是你碰巧遇到了它(指閃電事故),而是它選擇你”。
業務場景:
航空公司內部對于貨運單的價格管理,通常會頒發若干類型的運價文件,典型的有: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的配置,形成一條有規則的"鏈"即可。
文章列表