消除if-else/switch语句块来聚合模型的设计与实现

写在最前头的话:请不要理解为不再需要if-else/switch。写在最前头的结论:使用Enum。

1, 前言

if/switch这样的分支语句在实际开发中的使用自然是不可避免,但是我们必须承认使用这种分支判断语句实现的代码不仅可读性差(转来转去的绕晕),而且维护代价极高。导致维护代价上升,个人认为地并不是说是由于在开发软件时,开发人员基础不够好或者问题考虑不周全导致的各种漏洞和缺陷,主要原因是没有很好的遵循我们听烂了的软件开发基本原则-高内聚低耦合。在业务系统的开发过程中,大多业务需求都不可能是完全在一条分支完成的,而如果系统核心业务是要完成多个渠道、机构或者系统的对接,这时候应该怎么设计业务系统使得业务逻辑高度聚合呢?初始拿到这个需求的时候,脑子里可能会这样一个换面:你手持if/switch牧羊棍,驱使着千万头草尼玛。既然你的核心业务是要对接多个系统、而这些系统又是异构的,没有统一规范,if/switch是可以解决你的问题,但是它们很可能把你引入一个混乱的系统,因为它们已经把你的业务逻辑散落各处,到后面你的任何一个修改都可能是牵一发而动全身。

文字表述费劲、也不能具体说明问题,下面还是show my code吧。

2,问题描述

在这儿,设定一个业务场景。假定要开发一套对接渠道和交易所(或银行或登记结算中心,无论是渠道还是交易所都可以统一抽象为机构)的清算系统,对于清算系统的业务功能,具体可以拿我们大家都熟悉的支付宝这样的第三方支付为例来说明。渠道指的就是使用支付宝作为支付的商家,商家每天或者固定一个周期,会生成在其名下的交易流水信息文件(文件在不同场景的清算系统里文件种类多样),并将这些流水信息文件发给支付宝,然后支付宝接收并处理渠道端发送过来的流水信息文件,同时支付宝也要和背后的真正的资金管理方(银行)进行相关的文件交互(这里通常是和多家银行,每家银行有各自不同的文件交互规范)。因此,在这个清算系统里,一个问题是解决各类渠道和各个银行之间的文件交互问题(作为第三方系统,通常你无法让所有接入方都遵照你的一套规范来做)。抛开一些文件交互的实现细节问题,我们从业务角度分析其中的基本问题:文件类型多样、文件命名各异、文件存放路径各异。然后假定和交易所交互的文件模式形如transaction_000_product_(\d{8})_(\d{3}).req.txt(多类文件中的一类),和渠道交互的文件模式形如OFI_(\d{4})_8888_(\d{8}).TXT(多类文件中的一类)。

于是很可能会有如下代码场景:

 1 /**
 2 * 根据机构给定文件名模式,按照批次日期生成对应机构的交互文件名
 3 * @param institution
 4 * @param fileNamePattern
 5 * @param batchDate
 6 */
 7 public String fetchFileName(String institution, String fileNamePattern, Date batchDate) {
 8         String fileName = null;
 9         if(institution.equal("TA")) {
10             //交易所文件,8位日期,3位批次号
11             String fileKeyPatternWithDate = String
12                     .format(fileKeyPattern.replace("(\\d{8})", "%1$tY%1$tm%1$td"), new Date());
13             int seq = sequenceDAO.fetchSequence(fileKeyPatternWithDate);
14             fileName = fileKeyPatternWithDate.replace("(\\d{3})", String.format("%03d", seq));
15             fileName = fileName.substring(fileName.lastIndexOf(File.separator)+1);
16         } else if(institution.equal("Com")){
17             //渠道文件,8位日期,4位渠道号
18             fileName = String.format(fileKeyPattern.replace("(\\d{8})", "%1$tY%1$tm%1$td"), new Date());
19             fileName.replace("(\\d{4})", InstitutionCode.Com.code());
20         }
21         return fileName;
22 }
23
24 /**
25  * 可能有各种原因,公司没有为所有应用部署集中的ftp服务器或者纯粹只是建立一个临时方案,导致你需要针对不同的机构配置不同的ftp交互策略。
26  * 于是你可能有如下两个获取ftp download和upload目录的方法。
27 */
28 public String getDownloadFileDir(String institution, Date batchDate){
29    String date = new SimpleDateFormat("yyyyMMdd").format(batchDate);
30    //sftpBaseDir是使用map存放的关于各个机构交互文件的sftp父级目录
31    if(institution.equal("TA")){
32        return sftpBaseDir.get(institution) + "/" + date;
33     } else if(institution.equal("Com")){
34        return sftpBaseDir.get(institution) + "/download/" + date;
35     }
36
37    return null;
38 }
39 public String getUploadFileDir(String institution, Date batchDate){
40    String date = new SimpleDateFormat("yyyyMMdd").format(batchDate);
41    //sftpBaseDir是使用map存放的关于各个机构交互文件的sftp父级目录
42    if(institution.equal("TA")){
43        return sftpBaseDir.get(institution) + "/" + date;
44     } else if(institution.equal("Com")){
45        return sftpBaseDir.get(institution) + "/upload/" + date;
46     }
47     return null;
48 }
49
50 /**
51 * 由于文件模式固定,你的业务需求里很可能期望能通过对应机构的文件名就能推断该文件的一些详细信息.
52 */
53 public InstitutionFile inferFileInfoByFilename(String institution, String filename){
54  String filepattern = null;
55  if(institution.equal("TA")){
56   String patterns[] = fileName.split("\\d{8}",2);
57   String tmpPattern = patterns[0] + "(\\d{8})" + patterns[1];
58   patterns = tmpPattern.split("\\d{3}[.]", 2);
59   filepattern = patterns[0] + "(\\d{3})." + patterns[1];
60   } else if(institutuion.equal("Com")){
61   String patterns[] = fileName.split("\\d{8}", 2);
62   String tmpPattern = patterns[0] + "(\\d{8})" + patterns[1];
63   patterns = tmpPattern.split("\\d{4}", 2);
64   filepattern = patterns[0] + "(\\d{4})" + patterns[1];
65   }
66   InstitutionFile institutionFile = institutionRepository.
67          findFileByIdentity(institution.filePattern(file.getName()), institution);
68   return institutionFile;
69 }

代码段 1

如从上面列举的零散代码片段来看,可读性非常差。到处都是令人厌烦的分支判断(接入机构增多之后,情况会更糟),散落各处的业务处理规则和硬编码,完全没有拓展性可言。因此,我们有必要去认真地抽象和设计模型,使得业务逻辑更加聚合,代码更易维护和拓展。

3,抽象模型

图1 机构交互文件抽象模型

图1展示的领域模型的核心是Institution,对应的实现是enum类。Institution中针对每一个机构都定义了一份系统唯一的单例对象,所以每一个Institution对象也有在IFile中定义的,自己独立的文件交互业务的空间。InstitutionFile是作为系统内所有相关的机构文件的顶层抽象。

4,代码重构

下面看一下Institution核心实现,代码实际也很简单,基本都是提取自代码片段1中的代码。

Institution.java

  1 public enum Institution implements IInstitution, IFile {
  2
  3     TA("交易所"){
  4         @Override
  5         public Type type() {
  6             return Type.TA;
  7         }
  8
  9         @Override
 10         public String fileNameWithoutSeq(String filePattern, Date batchDate) {
 11             return String.format(filePattern.replace("(\\d{8})", "%1$tY%1$tm%1$td"), batchDate);
 12         }
 13
 14         @Override
 15         public String filePattern(String fileName) {
 16             String patterns[] = fileName.split("\\d{8}",2);
 17             String filePattern = patterns[0] + "(\\d{8})" + patterns[1];
 18             patterns = filePattern.split("\\d{3}[.]", 2);
 19             return patterns[0] + "(\\d{3})." + patterns[1];
 20         }
 21
 22         @Override
 23         public String downloadFilePath(String basePath, Date batchDate) {
 24             String date = new SimpleDateFormat("yyyyMMdd").format(batchDate);
 25             return basePath + "/" + date;
 26         }
 27
 28         @Override
 29         public String uploadFilePath(String basePath, Date batchDate) {
 30             String date = new SimpleDateFormat("yyyyMMdd").format(batchDate);
 31             return basePath + "/" + date;
 32         }
 33
 34         @Override
 35         public boolean multiBatch() {
 36             return true;
 37         }
 38
 39         @Override
 40         public String fileNameWithSeq(String fileNamePatternWithDate, int seq) {
 41             if(!multiBatch())
 42                 throw new UnsupportedOperationException("该机构不支持一天多批次文件");
 43             return fileNamePatternWithDate.replace("(\\d{3})", String.format("%03d", seq));
 44         }
 45     },
 46     Com("渠道") {
 47         @Override
 48         public Type type() {
 49             return Type.Channel;
 50         }
 51
 52         @Override
 53         public String fileNameWithoutSeq(String filePattern, Date batchDate) {
 54             String fileKeyPatternWithDate = String.format(filePattern.replace("(\\d{8})", "%1$tY%1$tm%1$td"),
 55                     batchDate);
 56             return fileKeyPatternWithDate.replace("(\\d{4})", ChannelCode.typeOf(this).getCode());
 57         }
 58
 59         @Override
 60         public String filePattern(String fileName) {
 61             String patterns[] = fileName.split("\\d{8}", 2);
 62             String filePattern = patterns[0] + "(\\d{8})" + patterns[1];
 63             patterns = filePattern.split("\\d{4}", 2);
 64             return patterns[0] + "(\\d{4})" + patterns[1];
 65         }
 66
 67         @Override
 68         public String fileNameWithSeq(String fileNamePatternWithDate, int seq) {
 69             throw new UnsupportedOperationException("该机构不支持一天多批次文件");
 70         }
 71     };
 72
 73     private String text;
 74     Institution(String text){
 75         this.text = text;
 76     }
 77     public String getText(){
 78         return this.text;
 79     }
 80     public String getAbbr(){
 81         return this.toString().toLowerCase();
 82     }
 83
 84     @Override
 85     public Institution type() {
 86         return this;
 87     }
 88
 89     @Override
 90     public InstitutionCode institutionCode() {
 91         return InstitutionCode.typeOf(this);
 92     }
 93
 94     public static Institution codeOf(String institution){
 95         for(Institution ins : Institution.values()){
 96             if(ins.getAbbr().equals(institution.toLowerCase())){
 97                 return ins;
 98             }
 99         }
100         throw new IllegalArgumentException("不支持机构");
101     }
102
103     @Override
104     public DateFormat fileNameDateFormat() {
105         return new SimpleDateFormat("yyyyMMdd");
106     }
107
108     @Override
109     public boolean multiBatch() {
110         return false;
111     }
112
113     @Override
114     public String downloadFilePath(String basePath, Date batchDate) {
115         String date = new SimpleDateFormat("yyyyMMdd").format(batchDate);
116         return basePath + "/download/" + date;
117     }
118
119     @Override
120     public String uploadFilePath(String basePath, Date batchDate) {
121         String date = new SimpleDateFormat("yyyyMMdd").format(batchDate);
122         return basePath + "/upload/" + date;
123     }
124
125 }

在Institution.java中,工作就是针对文件交互业务的抽象IFile,实现了根据不同的Institution来配置相对应的独立或者共享的文件交互策略。

最后,按照图1描述建立的领域模型,代码段1中的实现都将重构为对应的代码段2所示。对比代码片段1和2,重构后的代码变得更加简洁,我们也得到了聚合在一起的核心领域对象Institution。

 1 public String fetchFileName(Institution institution, String fileKeyPattern, Date batchDate) {
 2     String fileNamePatternWithDate = institution.fileNameWithoutSeq(fileKeyPattern, batchDate);
 3     if(institution.multiBatch()) {
 4         int seq = sequenceDAO.fetchSequence(institution + File.separator + fileNamePatternWithDate);
 5         return institution.fileNameWithSeq(fileNamePatternWithDate, seq);
 6     }
 7     return fileNamePatternWithDate;
 8 }
 9
10 public String getUploadFileDir(Institution institution, Date batchDate){
11     return institution.uploadFilePath(sftpBaseDir.get(institution.getAbbr()), batchDate);
12 }
13 public String getDownloadFileDir(Institution institution, Date batchDate){
14     return institution.downloadFilePath(sftpBaseDir.get(institution.getAbbr()), batchDate);
15 }
16
17 public InstitutionFile inferFileInfoByFilename(Institution institution, String filename){
18     InstitutionFile institutionFile = institutionRepository.
19                 findFileByIdentity(institution.filePattern(filename), institution);
20     return institutionFile;
21 }

代码段 2

时间: 2024-09-29 02:22:35

消除if-else/switch语句块来聚合模型的设计与实现的相关文章

switch语句和for循环

switch语句: 1. switch 后面小括号中表达式的值必须是整型或字符型 2. case后面的值可以是常量数值,如:1.日:也可以是一个常量表达式,如:2+2:但 不能是变量或带有变量的表达式,如:a*2 3. case匹配后,执行匹配块里的程序代码,如果没有遇见 break 会继续执行下一个的 case 块的内容,直到遇到 break 语句或者 switch 语句块结束 4.最后一条语句的break可以省略 5.default 块可以出现在任意位置,也可以省略 6.可以把功能相同的 c

c++中switch case语句多个值同个语句块写法

switch case语句: 1 switch(表达式) 2 { 3 case 常量表达式1: 4 { 5 语句块1: 6 break: 7 } 8 …… 9 case 常量表达式n: 10 { 11 语句块n: 12 break: 13 } 14 default: 15 { 16 语句块n+1: 17 } 18 } 当碰到多个常量使用同一语句块时,我习惯性用了pascal的写法,即如case 1..3,5这样子,而正确的写法应该是: 1 case 1:case 2:case 3: 2 { 3

蓝鸥Unity开发基础——Switch语句学习笔记

一.Switch语法 属于多分支语句,通过判断表达式的值,来决定执行哪个分支 Break用于结束某个case,然后执行switch之外的语句 Switch-开关:case-情况开关决定发生的情况 二.Switch基本语法 Switch(表达式){ Case 值1: 语句1 Break: Case 值2: 语句2 Break: -- Case 值n: 语句n Break: Default: 语句 Break: } 三.注意事项 整个defaul语句都可以舍掉,default语句最多只能由一个 Sw

黑马程序员---C基础3【变量的易错】【程序结构】【if语句】【Switch语句】

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- [变量的易错] 1.变量为什么要初始化为0 int  sum,a=3: sum = sum+a 如果未初始化则会成为一个不确定的变量,结果也会不确定,容易出错. 2.不同类型的变量之间的转换 切记int  a=1,b=0:b=1-1.5:其中b为一个整型所有结果是保留整数部分的0,而不是-0.5,又因为0没有正负之分,所有保存结果为b=0: 3.关于Xcode的一个快速注释的插件 快捷键://

利用switch语句计算特定的年份的月份共有几天。

//利用switch语句计算特定的年份的月份共有几天. let year =2015 let month =2 //先判断闰年中二月份的情况 ifmonth ==2 { if (year %400 ==0||year %4 ==0 &&year %100 !=0) //闰年的二月份29天,非闰年的二月份是28天 { print("29天") }else { print("28天") } } else { //考虑其他两种情况 switchmonth

条件判断语句 if语句/switch语句

if(条件表达式)语句 if语句即条件判断语句,对于if语句括号里的表达式,ECMAScript会自动调用Boolean()转型函数将这个表达式的结果转换成一个布尔值.如果值为true,执行后面的一条语句,否则不执行 <script type="text/javascript"> var box = 100; if (box > 50) //if 语句里的表达式如果返回的false,只会不执行后面的一条语句 alert(box); //第二条语句,和if语句无关,所以

if、switch语句

语句块 块:即为复合语句,指由一对花括号括起来的多条java语句. 语句块中可以包含另外一个语句块. 语句块决定了变量的作用域. 语句块中声明的变量只能在该语句块以及内部语句块中使用. 不能在嵌套的两个语句块中声明重名的变量. if语句 if语句是java语言选择控制或分支控制语句之一,用来对给定条件进行判定,并根据判定的结果(真或假)决定执行给出的两种操作之一. 单分支if语句:if(条件表达式){语句块} 双分支if语句:if(条件表达式){语句块1}else{语句块2} 多分支if语句:i

编译器--支持变量和语句块的计算器(二)

上篇文章记录了一个简单的计算器,但是只能计算一个表达式,比如计算8+3*5,得到值23.这次在其基础上添加了支持语句的功能,并且支持表达式中存在变量.比如下面: num1 := 5; num2 := num1+3*5; num3 := num1 * (num2 - 20/5); 最后计算并返回的值是num3的值80. 根据这个例子,可以看出相比于上次那个简单的计算器,添加的特性包括1.支持赋值语句  2.支持变量  3.支持多条赋值语句,也就是语句块.其中语句之间使用分号分隔,赋值符号为":=&

PHP Switch 语句

PHP 中的 Switch 语句用于执行基于多个不同条件的不同动作. 常使用 Switch 语句可以避免冗长的 if..elseif..else 代码块. 实例 工作原理: 对表达式(通常是变量)进行一次计算 把表达式的值与结构中 case 的值进行比较 如果存在匹配,则执行与 case 关联的代码 代码执行后,break 语句阻止代码跳入下一个 case 中继续执行 如果没有 case 为真,则使用 default 语句 <?php /*时间:2014-09-14 *作者:葛崇 *功能:Swi