郑晔·《代码之丑》学习笔记--长参数列表

Demon.Lee 2021年01月17日 2,465次浏览

1、函数间共享信息的两种方式

  • 参数列表

  • 全局变量

    从程序语言发展的过程来看,取消全局变量已经成为大势所趋。

    为什么?查阅相关资料,总结如下缺点:
    1)可读性差:上下文扩大,理解代码不易,没有局部变量清晰明了。
    2)可维护性差:如果多处更新共享变量,修改代码时容易遗漏,特别是后来入坑维护代码的小伙伴。
    3)耦合度高:全局变量被访问的范围越大,暗示着被耦合的程度越高,牵扯的东西越多,重构的难度也就越大。
    4)并发问题:若存在并发写的情况,容易出现线程安全问题。
    5)可测试性:会给单元测试带来麻烦。全局变量,尤其是静态局部变量,如果被测试的函数更新了该变量,而测试用例中并没有将其恢复到默认值,后续的单元测试很可能会测试失败。这还只是顺序方式,如果并发测试呢?更糟糕的是,被测试的类可能都没有提供重置该全局变量的接口。

    简单来说,“全局变量”是可变的,变的范围越大,越不可控,由此带来的风险也就越高。

2、长参数列表的问题

​ 作为函数间共享信息的两种方式之一的“全局变量”已被我们否定了,那就只剩一条路了:参数列表,但参数列表滥用也会带来问题。

  • 可读性差

    一时半会无法理解,因为大脑可以掌控的内容有限。

    如果参数列表超过3个字段,我们需要花不少的时间阅读对应的文档说明或翻阅代码才能看明白。

    Bob大叔在《代码整洁之道》中写道:

    最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)—— 所以无论如何也不要这么做。

    所以,当我再看到自己以前的代码,心情就可想而知了。

    public List<Instance> getNamespaceInstances(String name, String hostName, String status, String namespace, String labelKey, String labelValue) {
        String fieldSelector = this.getFieldSelector(name, hostName, status);
        String labelSelector = this.getLabelSelector(labelKey, labelValue);
        try {
          V1PodList list = this.api.listNamespacedPod(namespace, null, null, null, fieldSelector, labelSelector,null, null, null, null);
          return kubernetesModelAssembler.convertPodList2InstanceList(list.getItems(), this.gpuTypeList);
      } catch (ApiException e) {
          if (HttpStatus.SC_NOT_FOUND == e.getCode()) {
              return new ArrayList<>(0);
          }
          throw new KubernetesClientException(e, "listNamespacedPod", "get instances from kubernetes failed");
      }
    }
    
  • 可测试性

    另外一个很重要的角度是:可测试性。因为参数越多,编写单元测试用例的复杂度也就越高,想想三个入参变量与没有入参相比,输入变量的组合情况完全不是一个数量级的。

  • 可扩展性

    如果需求变化,需要对入参进行调整,比如增加一个参数。此时,要么你就需要就在原有函数上进行修改,被调用方都需要跟着调整——如果被调用处是你自己维护还好,否则你只能等着被数落。另外一种方式就是增加一个新的函数,只调整需要变化的地方。但无论哪种方式,灵活性都比较差。

3、解决方案

​ 前面讨论了长参数列表的各种问题,现在我们看看有哪些方式可以避免这一类坏味道的代码。

​ 软件设计应对的是需求变化,不变是我们最想要的,而变化却是常态。更进一步,对变化的部分可以根据变化频率继续细分。至此,我们对软件设计的理解就又上了一个层次。

  • 引入新的模型:将参数列表封装成对象

    针对变化频率相同的参数列表,如果这些参数都属于一个类,有相同的变化原因,将其封装为一个类是最好的解决方案。在大部分情况下,这是可行的。

  • 动静分离

    1)若变化频率只有两类:静态不变与动态变化,则可以将静态部分的参数重构为软件结构的一部分,比如类的成员变量。

    2)如果是多个不同的变化频率,则可以继续分离关注点,比如拆分多个类。

  • 告别标记参数

    标记参数不限于布尔类型,也可以是枚举值、数字或字符串等形式。

    标记参数到处传,简单来说也是一种if…else…逻辑,即函数内部根据这个标志参数走不同代码分支。但如果深入的想一想,其实这个标志位就意味着业务逻辑不同,所以,我们应该将其拆分为不同的函数。调用者只需要调用其业务逻辑所对应的那个函数即可,从而移除标记参数(Remove Flag Argument)。

    被调用者将业务进行拆分,移除了标记参数,但有这样一种情况:函数B调用函数C时,给C传了一个flag标志位,但B的这个flag不是B定义的,而是A传给B的,所以,我理解B也要进行业务拆分,这样层层拆分,业务就明确了。

    现在回头想想,自己经常重构类似的情况,也做的不到位,比如下面这段代码中getDns()方法:

    public class SiteInfo {
    
        private static final String HTTPS_HEADER = "https://";
        private static final String HTTP_HEADER = "http://";
    
        private String ipAddress;
        private Integer port;
    
        public String getDns() {
            return getDns(false);
        }
    
        public String getDns(boolean onlySsl) {
            String header = HTTPS_HEADER;
            String dnsSuffix = this.ipAddress;
            Integer port = this.getPort();
            if (null != port) {
                if (!onlySsl) {
                    header = HTTP_HEADER;
                }
                dnsSuffix += ":" + port;
            }
            return header + dnsSuffix;
        }
    }
    

    一开始只有一个getDns()方法,没有参数,后来需求变化,需要根据一个参数来判断返回的url头是http还是https。如果现在重新让我写的话,会调整为下面这样:

    public class Site {
    
      private static final String HTTPS_HEADER = "https://";
      private static final String HTTP_HEADER = "http://";
    
      private String ipAddress;
      private Integer port;
    
      public String getHttpDns() {
          return getDns(HTTP_HEADER);
      }
    
      public String getHttpsDns() {
          return getDns(HTTPS_HEADER);
      }
    
      private String getDns(String urlHeader){
          String dnsSuffix = getDnsSuffix();
          return urlHeader + dnsSuffix;
      }
    
      private String getDnsSuffix() {
          String dnsSuffix = this.ipAddress;
          if (isPortExist()) {
              dnsSuffix += ":" + this.port;
          }
          return dnsSuffix;
      }
    
      private boolean isPortExist(){
          return Optional.ofNullable(this.port).filter(val->val>0).isPresent();
      }
    }
    

    可能还不够好,继续迭代,继续进步。


课程原文:06 | 长参数列表:如何处理不同类型的长参数?