• 消除ifelse/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 }
    View Code
      在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
  • 相关阅读:
    T-SQL 查询出某个列总值大于X的数据
    ASP.NET 的IP帮助类
    对于一些Http远程连接Api安全的看法;
    老生常谈之SQL Server (行转列,列转行)
    关于SQL2008 “不允许保存更改。您所做的更改要求删除并重新创建以下表。您对无法重新创建的标进行了更改或者启用了‘阻止保存要求重新创建表的更改’” 解决方案
    linq to entity asp.net mvc 多字段排序
    MVC过滤器之添加LoginAttribute,浏览器提示:重定向次数太多
    层级多选框(html+javascript+bootstrap),全层全选和多选
    com.android.internal.os.ZygoteInit$MethodAndArgsCaller 解决
    Andriod Studio adb.exe,start-server' failed -- run manually if necessary 解决
  • 原文地址:https://www.cnblogs.com/shenjixiaodao/p/7230123.html
Copyright © 2020-2023  润新知