消除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