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(); } }
可能还不够好,继续迭代,继续进步。