问题背景
有某个颇为复杂的功能,功能拆分时把该功能拆分成了数十个步骤,每个步骤用一个方法来实现。需要依次调用数十个方法/函数,这些方法有相同的签名。
为了后期的维护和扩展,显然不可能像这样去调用:
step1(); // 第一步
step2(); // 第二步
...
stepn(); // 第n步
这样去调用的话,如果后期要在每个方法/函数后面都增加一个额外的功能(比如测量每个步骤的运行时间),那么工作量就翻了N倍,超出想象!
如果把这些方法直接或间接放入一个数组中,遍历这个数组,取出这些方法调用,代码就会变得非常简洁,也容易维护。如:
stepList = [step1, step, ..., stepn]; // 把这些步骤对应的方法都放入一个数组中
for(int i=0; i<n; i++) { // 遍历
stepList[i](); // 调用
}
围绕这个思路,我们来看看在具体编程语言中如何实现。这里以Java为例。当然,如果用 C、Javascript、PHP,会更简单。
各个步骤的示例代码如下:
为测试,在一个类中编写了3个方法,来代表需要执行的多个步骤。
public class TestAction {
/**
* 步骤一
*/
public boolean step1() {
System.out.println("step 1");
return false;
}
/**
* 步骤2
*/
public boolean step2() {
System.out.println("step 2");
return true;
}
/**
* 步骤3
*/
public boolean step3() {
System.out.println("step 3");
return true;
}
}
实现1. 反射 (Reflection)
把方法名字以字符串形式存放在一个数组,然后通过反射
(Reflection
)来获取到这个方法,再用invoke
调用该方法。
/**
* 方式1: 用反射批量调用方法 (该方法写在 TestAction 类中)
* @return
*/
public boolean doAllByReflect() {
Class claz = getClass();
String stepList[] = {
"step1", "step2", "step3" // 把方法名称放在数组里
};
boolean result = false;
try {
for(String methodName : stepList) {
Method method =claz.getMethod(methodName, null); // 通过反射获取到该方法
result = (boolean)method.invoke(this, null); // 调用
}
} catch (Exception e) { // 为缩减篇幅,具体异常的捕获和处理就不写了
e.printStackTrace();
}
return result;
}
测试输出
step 1
step 2
step 3
评价
优点:
- 如果需要追加步骤或者调整步骤顺序,只需要更改存储方法名称的数组就行了,简单方便。
- 如果需要在每个方法调用前/后增加处理逻辑,在
for
循环里面处理一次就行,工作量也不大。
缺点:
1.上面纯粹使用反射,把方法名字以字符串形式放入数组,编辑器和编译器无法发现拼写错误,只有在运行时才能发现错误。所以编译器会提示需要捕获4种异常:
- NoSuchMethodException 没有该方法。属于拼写错误导致的
- IllegalArgumentException 参数非法错误。调用时传入了错误类型/数量的参数时导致的
- InvocationTargetException 调用的方法出现异常。
- IllegalAccessException 非法访问异常。如果在同一个类内调用,不存在该问题。
所以如果代码编写不注意,容易埋下隐患。
实现2. 反射 + 注解 (Refelction + Annotation)
比之于通过方法的字符串名称来获取方法实例,可以给需要调用的方法用注解
(Annotation
)打上标签,然后用反射获取所有方法(getDeclaredMethods()
)并遍历,判断是否有该注解(isAnnotationPresent()
方法),有就调用。
实现如下:
2.1 定义Action
注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Action {
int sort(); // sort用来给方法排序,让方法按指定顺序调用,这里不考虑
}
2.2 把需要调用的方法加上Action注解
public class TestAction {
/**
* 步骤一
*/
@Action(sort=1)
public boolean step1() {
System.out.println("step 1");
return false;
}
/**
* 步骤2
*/
@Action(sort=2)
public boolean step2() {
System.out.println("step 2");
return true;
}
/**
* 步骤3
*/
@Action(sort=3)
public boolean step3() {
System.out.println("step 3");
return true;
}
}
2.3 调用
/**
* 方式2: 反射+注解 批量调用方法(该方法写在 TestAction 类中)
* @return
*/
public boolean doAllAnnotation() {
Class claz = getClass();
boolean result = false;
try {
Method[] methodList = claz.getDeclaredMethods();
for(Method method : methodList) {
// 如果该方法有Aaction注解,调用之
if (method.isAnnotationPresent(Action.class)) {
method.invoke(this, null);
}
}
} catch (Exception e) {
e.printStackTrace();
result = false;
}
return result;
}
测试输出
step 1
step 2
step 3
评价
优点:
- 如果需要追加步骤,增加相应的方法即可。
- 如果需要调整步骤顺序,调整注解中
sort
的值,在调用之前对其排序。 - 如果需要在每个方法调用前/后增加处理逻辑,在
for
循环里面处理一次就行,工作量也不大。
缺点:
- 调整顺序时需要增加额外的排序步骤。
- 如果注解被误用到了不是该步骤的方法上,会导致运行时出现问题。如果注解被误用到了不是该步骤的方法上,会导致运行时出现问题。
实现3. 利用Java8新特性: 方法引用和函数式接口
Java8提供了一些新特性,可以像函数式编程语言一样把方法当做一等公民,这两个便是 方法引用
(Method Reference
)和函数式接口
(Functional Interface
)。
首先利用函数式接口
让每个方法都具有相同的类型(从而可以放入同一个数组),然后利用方法引用
获取到该方法的引用,最后用 for循环
依次调用每个方法。
实现如下:
3.1 定义一个接口 IAction
@FunctionalInterface
public interface IAction {
boolean execute(); // 方法签名要与step系列方法的一样
}
3.2 调用
/**
* 方式3: 方法引用 + 函数式接口 (该方法写在 TestAction类中)
* @return
*/
public boolean doAll() {
IAction[] stepList = {
this::step1, // step1()方法的引用
this::step2,
this::step3
};
boolean result = false;
for(IAction action : stepList) {
action.execute(); // 执行该方法
}
return result;
}
在Java8中,还可以用stream
系列方法来代替上面的for
循环:
Arrays.asList(stepList).stream().forEach(step-> step.execute());
测试输出
step 1
step 2
step 3
评价
优点:
- 如果需要追加步骤或调整步骤顺序,修改
stepList
这个数组就行。 - 如果需要在每个方法调用前/后增加处理逻辑,在
for
循环里面处理一次就行,工作量也不大 - 与前面两个使用
反射
来获取到方法的引用,这里直接通过方法引用
,编辑器和编译器可以检查出潜在的错误,从而安全性较高。
缺点:
暂未发现明确的缺点。
4. 其它实现方式
当然也有其它方式来实现在一个循环中依次调用多个方法 ,比如:枚举
Enum,或者 把方法拆分到每个不同的类,每个类里的方法都是相同的名字和签名,然后创建每个类的实例放入一个数组,遍历,调用之。
由于这些实现方式的代码量较大,这里就不予举例了。
此文完。