zoukankan      html  css  js  c++  java
  • 设计模式之美学习-快速改善代码质量(十三)

    单元测试

    写单元测试,有时一个方法依赖其他很多服务 可以通过mock框架来实现 或者手动实现 比如依赖微服务UserSerevice 我们可以根据单元测试实现一个本地实现

    单元测试能够保证将来修改需求或者增加需求 判断是否有影响其他逻辑 当我们增加逻辑后只需要将原来的单元测试跑一遍

    解耦

    如何解耦

    封装抽象

    封装和抽象作为两个非常通用的设计思想,可以应用在很多设计场景中,比如系统、模块、lib、组件、接口、类等等的设计。封装和抽象可以有效地隐藏实现的复杂性,隔离实现的易变性,给依赖的模块提供稳定且易用的抽象接口。比如,Unix 系统提供的 open() 文件操作函数,我们用起来非常简单,但是底层实现却非常复杂,涉及权限控制、并发控制、物理存储等等。我们通过将其封装成一个抽象的 open() 函数,能够有效控制代码复杂性的蔓延,将复杂性封装在局部代码中。除此之外,因为 open() 函数基于抽象而非具体的实现来定义,所以我们在改动 open() 函数的底层实现的时候,并不需要改动依赖它的上层代码,也符合我们前面提到的“高内聚、松耦合”代码的评判标准。

    中间层

     引入中间层能简化模块或类之间的依赖关系。下面这张图是引入中间层前后的依赖关系对比图。在引入数据存储中间层之前,A、B、C 三个模块都要依赖内存一级缓存、Redis 二级缓存、DB 持久化存储三个模块。在引入中间层之后,三个模块只需要依赖数据存储一个模块即可。从图上可以看出,中间层的引入明显地简化了依赖关系,让代码结构更加清晰

    还有一个好处就是 比如我们一级缓存要替换成一个新的接口 那么我们所有依赖的地方都需要改,如果有中间层 我们只需要将中间层改成老的接口替换成新的接口

    模块化

     微服务模块化:商品服务 支付服务 库存服务 订单服务 等

    lib库模块化 比如我们的框架包的划分  比如dubbo  spring mvc

    命名

    利用上下文简化命名

    public class User {
        private String userName;
        private String userPassword;
        private String userAvatarUrl;
    }

    优化为

    public class User {
        private String Name;
        private String Password;
        private String AvatarUrl;
    }
    User user = new User();
    user.getName(); // 借助user对象这个上下文

    命名要可读、可搜索

    不要简写命名,大家都用“selectXXX”表示查询,你就不要用“queryXXX”;大家都用“insertXXX”表示插入一条数据,你就要不用“addXXX”,统一规约是很重要的,能减少很多不必要的麻烦

    如何命名接口和抽象类

    对于接口的命名,一般有两种比较常见的方式。一种是加前缀“I”,表示一个 Interface。比如 IUserService,对应的实现类命名为 UserService。

    另一种是不加前缀,比如 UserService,对应的实现类加后缀“Impl”,比如 UserServiceImpl。

    对于抽象类的命名,也有两种方式,一种是带上前缀“Abstract”,比如 AbstractConfiguration;另一种是不带前缀“Abstract”。实际上,对于接口和抽象类,选择哪种命名方式都是可以的,只要项目里能够统一就行。

    注释

    注释到底该写什么?

    注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。总结一下,注释的内容主要包含这样三个方面:做什么、为什么、怎么做

    /**
     * @author liqiang
     * @date 2020/2/13 16:07
     * *(what) 统一数据修复的service
     * * (why)  数据修复方法不散落在各个service
     * * (how)  凡是数据修复的相关方法都写在这里
     */
    public interface DataRepairService

    尽可能全面、详细,而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码的可读性。

    例子:我们项目代码里面的注释

    看到这个注释我有2个疑问

    1.为什么注释 这段代码有什么问题

    2.需要xxx解决什么问题

    类、函数多大才合适

    函数

    不要超过idea显示屏的高度,超过了可读性会比较差

    一行代码多长最合适

    一行代码最长不能超过 IDE 显示的宽度。需要横向滚动鼠标才能查看一行的全部代码,显然不利于代码的阅读。当然,这个限制也不能太小,太小会导致很多稍长点的语句被折成两行,也会影响到代码的整洁,不利于阅读

    善用空行分割单元块

     public void process(){
                //处理转账
                ......
                
                
                //记录流水
                ......
                
                
                //记录日志
                ......
            }

    除此之外,在类的成员变量与函数之间、静态成员变量与普通成员变量之间、各函数之间、甚至各成员变量之间,我们都可以通过添加空行的方式,让这些不同模块的代码之间,界限更加明确。写代码就类似写文章,善于应用空行,可以让代码的整体结构看起来更加有清晰、有条理。

    把代码分割成更小的单元块

    // 重构前的代码
    public void invest(long userId, long financialProductId) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(date);
      calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
      if (calendar.get(Calendar.DAY_OF_MONTH) == 1) {
        return;
      }
      //...
    }
    
    // 重构后的代码:提炼函数之后逻辑更加清晰
    public void invest(long userId, long financialProductId) {
      if (isLastDayOfMonth(new Date())) {
        return;
      }
      //...
    }
    
    public boolean isLastDayOfMonth(Date date) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(date);
      calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
      if (calendar.get(Calendar.DAY_OF_MONTH) == 1) {
       return true;
      }
      return false;
    }

    避免函数参数过多

    数包含 3、4 个参数的时候还是能接受的,大于等于 5 个的时候,我们就觉得参数有点过多了,会影响到代码的可读性,使用起来也不方便。针对参数过多的情况

    方式一

    考虑函数是否职责单一,是否能通过拆分成多个函数的方式来减少参数。

    public User getUser(String username, String telephone, String email);
    
    // 拆分成多个函数
    public User getUserByUsername(String username);
    public User getUserByTelephone(String telephone);
    public User getUserByEmail(String email); 

    方式二

    将函数的参数封装成对象

    public void postBlog(String title, String summary, String keywords, String content, String category, long authorId);
    
    // 将参数封装成对象
    public class Blog {
      private String title;
      private String summary;
      private String keywords;
      private Strint content;
      private String category;
      private long authorId;
    }
    public void postBlog(Blog blog);

    除此之外,如果函数是对外暴露的远程接口,将参数封装成对象,还可以提高接口的兼容性。在往接口中添加新的参数的时候,老的远程接口调用者有可能就不需要修改代码来兼容新的接口了

    勿用函数参数来控制逻辑

    不要在函数中使用布尔类型的标识参数来控制内部逻辑,true 的时候走这块逻辑,false 的时候走另一块逻辑。这明显违背了单一职责原则和接口隔离原则。

    如:

    public void buyCourse(long userId, long courseId, boolean isVip);
    
    // 将其拆分成两个函数
    public void buyCourse(long userId, long courseId);
    public void buyCourseForVip(long userId, long courseId);

    如果函数是 private 私有函数,影响范围有限,或者拆分之后的两个函数经常同时被调用,我们可以酌情考虑保留标识参数。

    函数设计要职责单一

    public boolean checkUserIfExisting(String telephone, String username, String email)  { 
      if (!StringUtils.isBlank(telephone)) {
        User user = userRepo.selectUserByTelephone(telephone);
        return user != null;
      }
      
      if (!StringUtils.isBlank(username)) {
        User user = userRepo.selectUserByUsername(username);
        return user != null;
      }
      
      if (!StringUtils.isBlank(email)) {
        User user = userRepo.selectUserByEmail(email);
        return user != null;
      }
      
      return false;
    }
    
    // 拆分成三个函数
    public boolean checkUserIfExistingByTelephone(String telephone);
    public boolean checkUserIfExistingByUsername(String username);
    public boolean checkUserIfExistingByEmail(String email);

    移除过深的嵌套层次

    例子一

    去掉多余的 if 或 else 语句

    // 示例一
    public double caculateTotalAmount(List<Order> orders) {
      if (orders == null || orders.isEmpty()) {
        return 0.0;
      } else { // 此处的else可以去掉
        double amount = 0.0;
        for (Order order : orders) {
          if (order != null) {
            amount += (order.getCount() * order.getPrice());
          }
        }
        return amount;
      }
    }
    
    // 示例二
    public List<String> matchStrings(List<String> strList,String substr) {
      List<String> matchedStrings = new ArrayList<>();
      if (strList != null && substr != null) {
        for (String str : strList) {
          if (str != null) { // 跟下面的if语句可以合并在一起
            if (str.contains(substr)) {
              matchedStrings.add(str);
            }
          }
        }
      }
      return matchedStrings;
    }

    例子二

    用编程语言提供的 continue、break、return 关键字,提前退出嵌套

    // 重构前的代码
    public List<String> matchStrings(List<String> strList,String substr) {
      List<String> matchedStrings = new ArrayList<>();
      if (strList != null && substr != null){ 
        for (String str : strList) {
          if (str != null && str.contains(substr)) {
            matchedStrings.add(str);
            // 此处还有10行代码...
          }
        }
      }
      return matchedStrings;
    }
    
    // 重构后的代码:使用continue提前退出
    public List<String> matchStrings(List<String> strList,String substr) {
      List<String> matchedStrings = new ArrayList<>();
      if (strList != null && substr != null){ 
        for (String str : strList) {
          if (str == null || !str.contains(substr)) {
            continue; 
          }
          matchedStrings.add(str);
          // 此处还有10行代码...
        }
      }
      return matchedStrings;
    }

    例子三 调整执行顺序来减少嵌套

    // 重构前的代码
    public List<String> matchStrings(List<String> strList,String substr) {
      List<String> matchedStrings = new ArrayList<>();
      if (strList != null && substr != null) {
        for (String str : strList) {
          if (str != null) {
            if (str.contains(substr)) {
              matchedStrings.add(str);
            }
          }
        }
      }
      return matchedStrings;
    }
    
    // 重构后的代码:先执行判空逻辑,再执行正常逻辑
    public List<String> matchStrings(List<String> strList,String substr) {
      if (strList == null || substr == null) { //先判空
        return Collections.emptyList();
      }
    
      List<String> matchedStrings = new ArrayList<>();
      for (String str : strList) {
        if (str != null) {
          if (str.contains(substr)) {
            matchedStrings.add(str);
          }
        }
      }
      return matchedStrings;
    }

    例子四 将部分嵌套逻辑封装成函数调用,以此来减少嵌套

    // 重构前的代码
    public List<String> appendSalts(List<String> passwords) {
      if (passwords == null || passwords.isEmpty()) {
        return Collections.emptyList();
      }
      
      List<String> passwordsWithSalt = new ArrayList<>();
      for (String password : passwords) {
        if (password == null) {
          continue;
        }
        if (password.length() < 8) {
          // ...
        } else {
          // ...
        }
      }
      return passwordsWithSalt;
    }
    
    // 重构后的代码:将部分逻辑抽成函数
    public List<String> appendSalts(List<String> passwords) {
      if (passwords == null || passwords.isEmpty()) {
        return Collections.emptyList();
      }
    
      List<String> passwordsWithSalt = new ArrayList<>();
      for (String password : passwords) {
        if (password == null) {
          continue;
        }
        passwordsWithSalt.add(appendSalt(password));
      }
      return passwordsWithSalt;
    }
    
    private String appendSalt(String password) {
      String passwordWithSalt = password;
      if (password.length() < 8) {
        // ...
      } else {
        // ...
      }
      return passwordWithSalt;
    }

    学会使用解释性变量

    public double CalculateCircularArea(double radius) {
      return (3.1415) * radius * radius;
    }
    
    // 常量替代魔法数字
    public static final Double PI = 3.1415;
    public double CalculateCircularArea(double radius) {
      return PI * radius * radius;
    }

    使用解释性变量来解释复杂表达式

    if (date.after(SUMMER_START) && date.before(SUMMER_END)) {
      // ...
    } else {
      // ...
    }
    
    // 引入解释性变量后逻辑更加清晰
    boolean isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);
    if (isSummer) {
      // ...
    } else {
      // ...
    } 
  • 相关阅读:
    Firefly是什么?有什么特点?
    windows7下启动mysql服务出现服务名无效
    win7系统64位eclipse环境超详细暗黑1.4服务器搭建
    Python安装模块出错(ImportError: No module named setuptools)解决方法
    Error format not a string literal and no format arguments解决方案
    DropFileName = "svchost.exe" 问题解决方案
    javascript
    Javascript
    PHP 命名空间namespace 和 use
    css
  • 原文地址:https://www.cnblogs.com/LQBlog/p/12303804.html
Copyright © 2011-2022 走看看