9.3.5 命令模式

9.3.5 命令模式

考虑这样一种场景:某个方法需要完成某一个功能,完成这个功能的大部分步骤已经确定了,但可能有少量具体步骤无法确定,必须等到执行该方法时才可以确定。具体一点:假设有个方法需要遍历某个数组的数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。
这个要求看起来有点奇怪:这个方法不仅要求参数可以变化,甚至要求方法执行体的代码也可以变化,需要能把“处理行为”作为一个参数传入该方法。
在某些编程语言(如RubyPerl等)中,确实允许传入一个代码块作为参数。从Java8开始支持的Lambda表达式也可以实现这种功能.
对于这样的需求,要求把”处理行为”作为参数传入该方法,而”处理行为”用编程来实现就是段代码。那如何把这段代码传入某个方法呢?在Java语言中,类才是一等公民,方法也不能独立存在,所以实际传入该方法的应该是一个对象,该对象通常是某个接口的匿名实现类的实例,该接口通常被称为命令接口,这种设计方式也被称为命令模式。

程序示例

1
2
3
4
5
6
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\Command
└─src\
├─Command.java
├─CommandTest.java
├─LambdaTest.java
└─ProcessArray.java

下面的程序先定义一个ProcessArray类,该类里包含一个each方法用于处理数组,但具体如何处理暂时不能确定,所以each()方法里定义了一个Command参数。

1
2
3
4
5
6
7
8
public class ProcessArray
{
// 定义一个each()方法,用于处理数组,
public void each(int[] target, Command cmd)
{
cmd.process(target);
}
}

上面定义each()方法时,指定了一个Command形参,这个Command接口用于定义一个process()方法,该方法用于封装对数组的”处理行为”。下面是该Command接口代码。

1
2
3
4
5
public interface Command
{
// 接口里定义的process()方法用于封装“处理行为”
void process(int[] target);
}

上面的Command接口里定义了一个process()方法,这个方法用于封装”处理行为”,但这个方法没有方法体—因为现在还无法确定这个处理行为。
下面是主程序调用ProcessArray对象each()方法的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CommandTest
{
public static void main(String[] args)
{
ProcessArray pa = new ProcessArray();
int[] target ={3, -4, 6, 4};
// 第一次处理数组,具体处理行为取决于Command对象
pa.each(target, new Command()
{
// 重写process()方法,决定具体的处理行为
public void process(int[] target)
{
for (int tmp : target)
{
System.out.println("迭代输出目标数组的元素:" + tmp);
}
}
});
System.out.println("------------------");
// 第二次处理数组,具体处理行为取决于Command对象
pa.each(target, new Command()
{
// 重写process()方法,决定具体的处理行为
public void process(int[] target)
{
int sum = 0;
for (int tmp : target)
{
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
}
});
}
}

正如上面的程序中两段粗体字代码所示,程序两次调用ProcessArray对象的each()方法来处理数组对象,每次调用each()方法时传入不同的Command匿名实现类的实例,不同的Command实例封装了不同的”处理行为”。
运行上面程序,将看到如下结果。

1
2
3
4
5
6
迭代输出目标数组的元素:3
迭代输出目标数组的元素:-4
迭代输出目标数组的元素:6
迭代输出目标数组的元素:4
------------------
数组元素的总和是:9

上面的运行结果显示了两次不同处理行为的结果,也就实现了process()方法和”处理行为”的分离,两次不同的处理行为分别由两个不同的Command对象来提供。

Java8新增了Lambda表达式功能,Java8允许使用Lambda表达式创建函数式接口的实例。

什么是函数式接口

所谓函数式接口,指的是只包含一个抽象方法的接口。上面程序中的Command接口就是函数式接口。因此CommandTest可改写为如下形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LambdaTest
{
public static void main(String[] args)
{
ProcessArray pa = new ProcessArray();
int[] target ={3, -4, 6, 4};
// 第一次处理数组,具体处理行为取决于Lambda表达式
pa.each(target, array ->
{
for (int tmp : array)
{
System.out.println("迭代输出目标数组的元素:" + tmp);
}
});
System.out.println("------------------");
// 第二次处理数组,具体处理行为取决于Lambda表达式
pa.each(target, array ->
{
int sum = 0;
for (int tmp : array)
{
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
});
}
}

理解了这个命令模式后,相信读者对Spring框架中HibernateTemplateexecute方法找到了一点感觉, HibernateTemplate使用了execute()方法弥补了HibernateTemplate的不足,该方法需要接受一个HibernateCallback接口,该接口的代码如下:

1
2
3
4
5
// 定义一个`HibernateCallback`接口,该接口封装持久化处理行为
public interface HibernateCallback
{
Object doInHibernate(Session session);
}

上面的HibernateCallback接口就是一个典型的Command接口,一个HibernateCallback对象封装了自定义的持久化处理
HibernateTemplate而言,大部分持久化操作都可通过一个方法来实现, HibernateTemplate对象简化了Hibernate的持久化操作,但丢失了使用Hibernate持久化操作的灵活性.
通过HibernateCallback就可以弥补HibernateTemplate灵活性不足的缺点,当调用HibernateTemplateexecute()方法时,传入Hibernate Callback对象的dolnhibernateo方法就是自定义的持久化处理—即将自定义的持久化处理传入了execute()方法。下面的代码片段使用Lambda表达式来实现该功能。

1
2
3
4
5
6
7
8
9
10
List list=getHibernateTemplate()
.execute(session ->{
//执行 Hibernate分页查询
List result=session.createQuery(hql)
.setFirstResult(offset);
.setMaxResults(pageSize);
.list();
return result;
});
return list;

上面程序中的粗体字代码块将直接传给HibernatTemplate,HibernatTemplate将直接使用该代码块来执行持久化查询,并将查询得到的结果作为execute()方法的返回值。