用面向对象思维重构过程式代码
一、背景
有一个自动化执行测试案例的程序,需要根据用户输入的参数决定大量案例中的哪些案例需要执行。程序代码如下(为便于理解,这里仅贴出与主题相关的代码):
//处理指定参数组的情况 [A,B,C]即指执行参数组(A,B,C)或者[A:C]即指执行参数组(A,B,C)
private void _run(Config config, TestEngine te, TestNotifier fNotifier, TestCase tc ,String paramGroupName) throws Exception{
boolean isScope = false;
String[] paramGroupNames = null;
if(paramGroupName != null && paramGroupName.trim().length() != 0){
if(paramGroupName.indexOf(",") != -1){
paramGroupNames = paramGroupName.split("[,]+");
}else if(paramGroupName.indexOf(":") != -1){
isScope = true;
paramGroupNames = paramGroupName.split("[:]+");
}else {
paramGroupNames = new String[]{paramGroupName};
}
}
boolean isRun = false;
for (TestParamGroup tpg : tc.getParams()) {
if(paramGroupNames == null){
run(config, te, fNotifier, tc, tpg);
//如果指定的具体生成那个参数组,则只执行该参数组对应的案例
}else{
//如果是定义的是范围 A:C
if(isScope){
//如果已经超出范围,直接跳出循环
if(isRun){
run(config, te, fNotifier, tc, tpg);
if(paramGroupNames[1].equals(tpg.getName())) break;
}else if(paramGroupNames[0].equals(tpg.getName())){
run(config, te, fNotifier, tc, tpg);
isRun = true;
}
}else{
for(String paramName : paramGroupNames)
if(paramName.trim().equals(tpg.getName().trim())){
run(config, te, fNotifier, tc, tpg);
break;
}
}
}
}
}
代码比较长,我简单解释一下:
- _run方法接受参数Config、TestEngine、TestNotifier、TestCase四个对象实现指定案例的执行、结果通知等功能;
- 参数paramGroupName就是用户从命令行指定的用于过滤案例的条件。该参数的格式支持A,C,E,F和C:F两种格式(即以逗号分隔的列表和以冒号分隔的范围)
- 整个方法的逻辑分为两段:以boolean isRun = false;为界,前半段代码解析用户输入的参数;后半段代码则判断并仅运行满足条件的案例。
二、问题
上面的代码有什么问题吗?
没有问题!确实没有问题,那段代码在我们发布的产品中使用了大半年,没有出现任何问题!
能把这篇文章看到这里的同学已经非常难得了,不过我想问一个问题:有多少人真的看了上面的代码并且看懂了?我想一定非常少吧!
不要自责,我自己看那段代码也花了好几分钟,前前后后分析了几遍才真正搞懂。而这正是这段代码的问题所在:方法过大、参数解析逻辑和过滤逻辑混杂不清。当我们需要增加新功能或修改需求的时候,这样的代码带来的隐性成本其实是不可忽视的!
除了上面的问题之外,这段代码的可维护性也是大问题。设想,如果我想要支持A,B,C:D的匹配(即列表和范围混合)呢?如果我想要支持C:或:D(即不设上限或下限)的范围匹配需求呢?任何一种需求的变更,都会立即导致上面的代码结构变得更加复杂深奥难于理解。
三、方案
下面就动用我们的“重构手术刀”将上面的代码整理一下吧。重构的总体思路如下:
- 将该方法中的参数解析逻辑分离出来;
- 将判断逻辑与案例遍历逻辑分离出来;
上面的总体思路没有问题:第一步是针对问题对症下药;第二步是让这个药好吃一点,同时为以后的良好体质打下基础。
但具体如何实施呢?直接大刀阔斧实施代码大挪移,将一个方法拆分为三个方法,可以吗?当然可以,但最后出来的代码又会有新的问题:三个方法间代码逻辑藕合度高,可理解性及可维护性并不见得比原来的强多少。(具体改造的代码我就不贴出来了,大家可以自行练习比较)
四、实施
下面是我们运用对象思维重构上面那段代码的结果。
//处理指定参数组的情况 [A,B,C]即指执行参数组(A,B,C)或者[A:C]即指执行参数组(A,B,C)
private static interface ParamMatcher {
public boolean match(TestParamGroup tpg);
}
核心对象是ParamMatcher,参数匹配器,用于实现案例参数匹配逻辑。设计为接口interface而不是类class的原因是,我们会有多种参数匹配逻辑。如需求中的列表匹配逻辑可用如下代码实现:
//如果指定的具体生成那个参数组,则只执行该参数组对应的案例
private static class ListParamMatcher implements ParamMatcher {
final private Set paramNames;
private ListParamMatcher(Set paramNames) {
this.paramNames = paramNames;
}
@Override public boolean match(TestParamGroup tpg) {
return paramNames.contains(tpg.getName().trim());
}
}
需求中的范围匹配可用下面的代码实现:
//如果是定义的是范围 A:C
private static class ScopeParamMatcher implements ParamMatcher {
final private String beginParamName, endParamEnd;
private ScopeParamMatcher(String beginParamName, String endParamEnd) {
this.beginParamName = beginParamName;
this.endParamEnd = endParamEnd;
}
@Override public boolean match(TestParamGroup tpg) {
if (beginParamName != null && beginParamName.compareTo(tpg.getName()) > 0)
return false;
if (endParamEnd != null && endParamEnd.compareTo(tpg.getName()) < 0)
return false;
return true;
}
}
上面的代码其实已经实现了不设上限或不设下限的情况。
当然,我们还有匹配所有的逻辑实现:
private static class AllParamMatcher implements ParamMatcher {
@Override public boolean match(TestParamGroup tpg) {
return true;
}
}
根据以后匹配逻辑的增加,我们还可以增加相应的实现。
下面来看参数解析逻辑代码分离,重构后的代码如下:
private static ParamMatcher parse(String paramGroupName) {
if (paramGroupName != null && paramGroupName.trim().length() != 0){
if(paramGroupName.indexOf(",") != -1) {
String[] paramGroupNames = paramGroupName.split("[,\\s]+");
return new ListParamMatcher(new HashSet(Arrays.asList(paramGroupNames)));
} else if (paramGroupName.indexOf(":") != -1) {
int index = paramGroupName.indexOf(":");
String begin = paramGroupName.substring(0, index);
String end = paramGroupName.substring(index+1);
return new ScopeParamMatcher(begin, end);
} else {
return new ListParamMatcher(new HashSet(Arrays.asList(paramGroupName)));
}
}
else {
return new AllParamMatcher();
}
}
上面的代码就是先前代码中的前半段代码拷贝,但其性质变成了负责创建ParamMatcher的工厂方法。而我们重构的对象现在变成什么样子了?看代码:
//处理指定参数组的情况 [A,B,C]即指执行参数组(A,B,C)或者[A:C]即指执行参数组(A,B,C)
private int _run(Config config, TestEngine te, TestNotifier fNotifier, TestCase tc, ParamMatcher pm) throws Exception{
int count = 0;
for (TestParamGroup tpg : tc.getParams()) {
if (pm.match(tpg)) {
count += run(config, te, fNotifier, tc, tpg);
}
}
return count;
}
只剩下了案例循环逻辑和执行逻辑,代码非常简洁,一目了然。
五、总结
我们前面通过一个实际的案例展示了如何运用面向对象的思想重构一段过程化的代码,对象化的代码和过程化的代码两者在实现的逻辑功能上是完全一致的,但面向对象方式通过职责的分离和再分配,实现了逻辑的解藉,最终达到了 更好的可理解性和可维护性的目的。
这如同一家公司,当其业务简单单一的时候,不需要划分部门就可以很好地处理业务。随着业务量的增长,我们可以通过对象的方式将职责分配给不同的部门,通过部门间的协同共同实现原有的业务逻辑。而此基础之上,各部门(对象或模块)就可以相对独立地发展(维护),更好地支持业务(功能)的拓展。