设计原则与思想:规范与重构


设计原则与思想:规则与重构

理论一:重构的why、what、when、how

重构的目的:为什么重构(why)?

  • 对于项目而言,重构可以保持代码质量持续处于一个可控状态,不至于太糟糕(无可救药)
  • 对于个人而言,重构非常锻炼一个人的代码能力(很有成就感)

重构的对象:重构什么(what)?

  • 大规模高层次的重构

    • 代码分层
    • 模块化
    • 解耦
    • 梳理类之间的交互关系
    • 抽象复用组件
  • 小规模低层次的重构

    • 规范命名
    • 注释
    • 修正函数参数过多
    • 消除超大类
    • 提取重复代码

重构的时机:什么时候重构(when)?

  • 建立持续重构意识
  • 重构要融入日常开发中
  • 而非等到代码出现大问题才重构

重构的方法:如何重构(how)?

  • 大规模高层次的重构
    • 有组织、有计划地进行
    • 分阶段地小步快跑
    • 时刻让代码处于一个可运行的状态
  • 小规模低层次的重构
    • 随时随地

理论二:单元测试(保证重构不出错的技术手段)

  • What
    • 代码层面的测试,用来测试编写代码逻辑的正确性
    • 单元一般是类或函数,而不是模块或者系统
  • Why
    • 单元测试的过程本身就是代码Code Review和重构的过程,能有效发现代码中的BUG和代码设计上的问题
    • 单元测试集成测试的有力补充
    • 能帮助我们快速熟悉代码
  • How
    • 针对代码设计各种测试用例,以覆盖各种输入、异常、边界情况,并翻译成代码
    • 单元测试不要依赖被测代码的具体实现逻辑
    • 难以编写单元测试,往往是代码的可测试性不好(说明要重构了)
  • Example
    @RunWith(SpringRunner.class)
    @SppringBootTest
    public class UserTest {
      @Autowired
      private UserService userService;
    
      @Test
      public void getUser(){
        //测试getUserById接口是否正常
        User user = userService.getUserById(111L);
        //user不为空断言(如果为空会报错)
        Assert.assertNotNull(user);
      }
    }

理论三:代码的可测试性

  • What:代码的可测试性,就是针对代码编写单元测试的难易程度。
  • How依赖注入是编写可测试性代码的最有效手段。(减少不必要的rpc耗时+自定义返回内容)
    /**
     * 一个电商系统的交易类
     */
    public class Transaction {
    
      //依赖的外部rpc接口(可能耗时长,不便于单测)
      private WalletRpcService walletRpcService;
    
      //通过外部set方法实现依赖注入(传递参数)
      public void setWalletRpcService(WalletRpcService walletRpcService) {
        this.walletRpcService = walletRpcService;
      }
    
      //交易方法
      public boolean execute(){
          walletRpcService.handle();
          //...
      }
    }
    
    /**
     * mock的rpc
     */
    public class MockWalletRpcService extends WalletRpcService {
        //...
    }
    
    /**
     * 单元测试
     */
    public class TransactionTest {
      @Test
      public void testExecute() {
        Transaction transaction = new Transaction();
        //这里执行我们mock的类(可以自定义返回内容,而非调用实际的WalletRpcService返回不可控的内容以及不可控的耗时)
        transaction.setWalletRpcService(new MockWalletRpcService());
    
        boolean result = transaction.execute();
        //断言,如果result不为true则报错
        assertTrue(result);
      }
    }

常见的Anti-Patterns(反面模式)

  • 1.代码中包含未决行为逻辑(代码return是随机/不确定的)

    public class Demo {
      /**
       * 计算入参早于当前时间多少天
       * @param dueTime
       * @return 每天返回的值不同
       */
      public long calculateDelayDays(Date dueTime) {
        long currentTime = System.currentTimeMillis();
        if(dueTime.getTime() >= currentTime) {
          return 0;
        }
    
        long delayTime = currentTime - dueTime.getTime();
        long delayDays = delayTime / 86400;
        return delayDays;
      }
    }
  • 2.滥用可变全局变量(容易影响其他测试用例)
    如果单元测试方法testAdd()和testReduce()是并发执行的,那么可能在assertEquals执行之前value就执行了一次add()和一次reduce(),那么结果就跟我们预期的不一样了

    /**
     * 简易计算器
     */
    public class Counter {
      public static int value = 0;
    
      //加法
      public boolean add(int addValue) {
      }
    
      //减法
      public boolean reduce(int reduceValue) {
      }
    
      public static int getValue() {
        return value;
      }
    }
    
    /**
     * 单元测试
     */
    public class CounterTest {
      public void testAdd() {
        Counter counter = new Counter();
        counter.add(2);
        assertEquals(2, counter.getValue());
      }
    
      public void testReduce() {
        Counter counter = new Counter();
        counter.reduce(2);
        assertEquals(-2, counter.getValue());
      }
    }
  • 3.滥用静态方法(跟2.滥用可变全局变量类似)

  • 4.使用复杂的继承关系(如果父类需要mock某个依赖对象,那么子类、子类的子类…都需要mock这个依赖对象,会很复杂)

  • 5.高耦合代码(如果一个类依赖几十个外部对象,那我们编写单元测试的时候就可能需要mock几十个外部对象,复杂且不合理)

理论四:代码解耦

  • 为什么要解耦?
    • 过于复杂的代码往往可读性、可维护性不友好(解耦可以保证代码质量)
    • 保证代码松耦合、高内聚(能有效控制代码复杂度)
  • 怎么看是否需要解耦?
    • 直接的衡量标准是把模块与模块、类与类自己的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重构
  • 怎么重构?
    • 封装与抽象
    • 中间层(如mq)
    • 模块化
    • 一些设计思想与原则
      • 单一职责原则
      • 基于接口而非实现编程
      • 依赖注入
      • 多用组合少用继承
      • 迪米特法则(不是很懂。。。。)
    • 设计模式(如观察者模式)

理论五:快速改善代码质量的20条编程规范

跳转查看:《快速改善代码质量的20条编程规范》

实战一:如何发现代码质量问题

常规checklist
业务checklist

实战二:如何处理程序出错的返回

  • 1.返回错误码
    • 是C语言最常用的出错处理方式
    • Java语言极少会用到错误码(异常)
  • 2.返回NULL值
    • 用来表示”不存在”这种语义
    • 对于查找函数(如getxxx、queryxxx)来说,数据不存在是一种正常行为(并非异常情况),所以返回NULL值更加合理
public class UserService {
  public User getUserById(int id) {
    //如果用户不存在,则返回null
    return null;
  }
}
  • 3.返回空对象
    • 针对2.返回NULL值的弊端:调用方容易不做判空导致出现NPE异常
    • 对于字符串类型或者集合类型时,我们可以用空字符串或空集合替代NULL值(表示不存在)
  • 4.抛出异常对象
    • 直接吞掉(原地catch并打印错误日志)
    • 直接往上抛出(当前方法不处理)
    • 包裹成新的异常抛出(包装成通用的异常返回,不暴露实现细节)

文章作者: GaryLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 GaryLee !
  目录