32.1 命令模式VS策略模式

32.1 命令模式VS策略模式

命令模式和策略模式的类图确实很相似,只是命令模式多了一个接收者(Receiver)角色。它们虽然同为行为类模式,但是两者的区别还是很明显的。策略模式的意图是封装算法,它认为“算法”已经是一个完整的、不可拆分的原子业务(注意这里是原子业务,而不是原子对象),即其意图是让这些算法独立,并且可以相互替换,让行为的变化独立于拥有行为的客户;而命令模式则是对动作的解耦,把一个动作的执行分为执行对象(接收者角色)、执行行为(命令角色),让两者相互独立而不相互影响。

我们从一个相同的业务需求出发,按照命令模式和策略模式分别设计出一套实现,来看看它们的侧重点有什么不同。zip和gzip文件格式相信大家都很熟悉,它们是两种不同的压缩格式,我们今天就来对一个目录或文件实现两种不同的压缩方式:zip压缩和gzip压缩(这里的压缩指的是压缩和解压缩两种对应的操作行为,下同)。实现这两种压缩格式有什么意义呢?有意义!一是zip格式(.zip后缀)是Windows操作系统常用的压缩格式,gzip格式(.gz 后缀)是*nix系统常用的压缩格式;二是JDK提供了对zip和gzip文件的操作包,非常容易实现文件的压缩和解压缩操作。

下面我们来实现不同格式的压缩和解压缩功能。

32.1.1 策略模式实现压缩算法

使用策略模式实现压缩算法非常简单,也是非常标准的,类图如图32-1所示。

在类图中,我们的侧重点是zip压缩算法和gzip压缩算法可以互相替换,一个文件或者目录可以使用zip压缩,也可以使用gzip压缩,选择哪种压缩算法是由高层模块(实际操作者) 决定的。我们来看一下代码实现。先看抽象的压缩算法,如代码清单32-1所示。

image-20210930163955571

图32-1 策略模式实现压缩算法的类图

代码清单32-1 抽象压缩算法

1
2
3
4
5
6
public interface Algorithm {
//压缩算法
public boolean compress(String source,String to);
//解压缩算法
public boolean uncompress(String source,String to);
}

每一个算法要实现两个功能:压缩和解压缩,传递进来一个绝对路径source,compress 把它压缩到to目录下,uncompress则进行反向操作——解压缩,这两个方法一定要成对地实现,为什么呢?用gzip解压缩算法能解开zip格式的压缩文件吗?我们分别来看两种不同格式的压缩算法,zip、gzip压缩算法分别如代码清单32-2、代码清单32-3所示。

代码清单32-2 zip压缩算法

1
2
3
4
5
6
7
8
9
10
11
12
public class Zip implements Algorithm {
//zip格式的压缩算法
public boolean compress(String source, String to) {
System.out.println(source + " --> " +to + " ZIP压缩成功!");
return true;
}
//zip格式的解压缩算法
public boolean uncompress(String source,String to){
System.out.println(source + " --> " +to + " ZIP解压缩成功!");
return true;
}
}

代码清单32-3 gzip压缩算法

1
2
3
4
5
6
7
8
9
10
11
12
public class Gzip implements Algorithm {
//gzip的压缩算法
public boolean compress(String source, String to) {
System.out.println(source + " --> " +to + " GZIP压缩成功!");
return true;
}
//gzip解压缩算法
public boolean uncompress(String source,String to){
System.out.println(source + " --> " +to + " GZIP解压缩成功!");
return true;
}
}

这两种压缩算法实现起来都很简单,Java对此都提供了相关的API操作,这里就不再提供详细的编写代码,读者可以参考JDK自己进行实现,或者上网搜索一下,网上有太多类似的源代码。

两个具体的算法实现了同一个接口,完全遵循依赖倒转原则。我们再来看环境角色,如代码清单32-4所示。

代码清单32-4 环境角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Context {
//指向抽象算法
private Algorithm al;
//构造函数传递具体的算法
public Context(Algorithm _al){
this.al = _al;
}
//执行压缩算法
public boolean compress(String source,String to){
return al.compress(source, to);
}
//执行解压缩算法
public boolean uncompress(String source,String to){
return al.uncompress(source, to);
}
}

也是非常简单,指定一个算法,执行该算法,一个标准的策略模式就编写完毕了。请读者注意,这里虽然有两个算法Zip和Gzip,但是对调用者来说,这两个算法没有本质上的区别,只是“形式”上不同,什么意思呢?从调用者来看,使用哪一个算法都无所谓,两者完全可以互换,甚至用一个算法替代另外一个算法。我们继续看调用者是如何调用的,如代码清单32-5所示。

代码清单32-5 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Client {
public static void main(String[] args) {
//定义环境角色
Context context;
//对文件执行zip压缩算法
System.out.println("========执行算法========");
context = new Context(new Zip());
/*
*算法替换
* context = new Context(new Gzip());
*
*/
//执行压缩算法
context.compress("c:\\windows","d:\\windows.zip");
//执行解压缩算法
context.uncompress("c:\\windows.zip","d:\\windows");
}
}

运行结果如下所示:

1
2
3
========执行算法======== 
c:\windows --> d:\windows.zip ZIP 压缩成功!
c:\windows.zip --> d:\windows ZIP 解压缩成功!

要使用gzip算法吗?在客户端(Client)上把注释删掉就可以了,其他的模块根本不受任何影响,策略模式关心的是算法是否可以相互替换。策略模式虽然简单,但是在项目组使用得非常多,可以说随手拈来就是一个策略模式。

32.1.2 命令模式实现压缩算法

命令模式的主旨是封装命令,使请求者与实现者解耦。例如,到饭店点菜,客人(请求者)通过服务员(调用者)向厨师(接收者)发送了订单(行为的请求),该例子就是通过封装命令来使请求者和接收者解耦。我们继续来看压缩和解压缩的例子,怎么使用命令模式来完成该需求呢?我们先画出类图,如图32-2所示。

类图看着复杂,但是还是一个典型的命令模式,通过定义具体命令完成文件的压缩、解压缩任务,注意我们这里对文件的每一个操作都是封装好的命令,对于给定的请求,命令不同,处理的结果当然也不同,这就是命令模式要强调的。我们先来看抽象命令,如代码清单32-6所示。

代码清单32-6 抽象压缩命令

1
2
3
4
5
6
7
public abstract class AbstractCmd {
//对接收者的引用
protected IReceiver zip = new ZipReceiver();
protected IReceiver gzip = new GzipReceiver();
//抽象方法,命令的具体单元
public abstract boolean execute(String source,String to);
}

image-20210930164353477

图32-2 命令模式实现压缩算法的类图

抽象命令定义了两个接收者的引用:zip接收者和gzip接收者,大家可以想象一下这两个“受气包”,它们完全是受众,人家让它干啥它就干啥,具体使用哪个接收者是命令决定的。具体命令有4个:zip压缩、zip解压缩、gzip压缩、gzip解压缩,分别如代码清单32-7、 32-8、32-9、32-10所示。

代码清单32-7 zip压缩命令

1
2
3
4
5
public class ZipCompressCmd extends AbstractCmd {
public boolean execute(String source,String to) {
return super.zip.compress(source, to);
}
}

代码清单32-8 zip解压缩命令

1
2
3
4
5
public class ZipUncompressCmd extends AbstractCmd {
public boolean execute(String source,String to) {
return super.zip.uncompress(source, to);
}
}

代码清单32-9 gzip压缩命令

1
2
3
4
5
public class GzipCompressCmd extends AbstractCmd {
public boolean execute(String source,String to) {
return super.gzip.compress(source, to);
}
}

代码清单32-10 gzip解压缩命令

1
2
3
4
5
public class GzipUncompressCmd extends AbstractCmd {
public boolean execute(String source,String to) {
return super.gzip.uncompress(source, to);
}
}

它们非常简单,都只有一个方法,坚决地执行命令,使用了委托的方式,由接收者来实现。我们再来看抽象接收者,如代码清单32-11所示。

代码清单32-11 抽象接收者

1
2
3
4
5
6
public interface IReceiver {
//压缩
public boolean compress(String source,String to);
//解压缩
public boolean uncompress(String source,String to);
}

抽象接收者与策略模式的抽象策略完全相同,具体的实现也完全相同,只是类名做了改动,我们先来看zip压缩的实现,如代码清单32-12所示。
代码清单32-12 zip接收者

1
2
3
4
5
6
7
8
9
10
11
12
public class ZipReceiver implements IReceiver {
//zip格式的压缩算法
public boolean compress(String source, String to) {
System.out.println(source + " --> " +to + " ZIP压缩成功!");
return true;
}
//zip格式的解压缩算法
public boolean uncompress(String source,String to){
System.out.println(source + " --> " +to + " ZIP解压缩成功!");
return true;
}
}

这就是一个具体动作执行者,它在策略模式中是一个具体的算法,关心的是是否可以被替换;而在命令模式中,它则是一个具体、真实的命令执行者。我们再来看gzip接收者,如代码清单32-13所示。

代码清单32-13 gzip接收者

1
2
3
4
5
6
7
8
9
10
11
12
public class GzipReceiver implements IReceiver {
//gzip的压缩算法
public boolean compress(String source, String to) {
System.out.println(source + " --> " +to + " GZIP压缩成功!");
return true;
}
//gzip解压缩算法
public boolean uncompress(String source,String to){
System.out.println(source + " --> " +to + " GZIP解压缩成功!");
return true;
}
}

大家可以这样思考这个问题,接收者就是厨房的厨师,具体要哪个厨师做这道菜则是餐馆的规章制度已经明确的,你让专做粤菜的师傅做一个剁椒鱼头,能做出好菜吗?在命令模式中,就是在抽象命令中定义了接收者的引用,然后在具体的实现类中确定要让哪个接收者进行处理。这就好比是客人点菜:我要一个剁椒鱼头,这就是一个命令,然后服务员 (Inovker)接收到这个命令后,就开始执行,把这个命令指定给具体的执行者执行。

当然了,接收者这部分还可以这样设计,即按照职责设计接收者,比如压缩接收者、解压缩接收者,但接口需要稍稍改动,如代码清单32-14所示。

代码清单32-14 依照职责设计的接收者接口

1
2
3
4
5
6
public interface IReceiver {
//执行zip命令
public boolean zipExec(String source,String to);
//执行gzip命令
public boolean gzipExec(String source,String to);
}

接收者接口只是定义了每个接收者都必须完成zip和gzip相关的两个逻辑,有多少个职责就有多少个实现类。我们这里只有两个职责:压缩和解压缩,分别如代码清单32-15、32-16 所示。

代码清单32-15 压缩接收者

1
2
3
4
5
6
7
8
9
10
11
12
public class CompressReceiver implements IReceiver {
//执行gzip压缩命令
public boolean gzipExec(String source, String to) {
System.out.println(source + " --> " +to + " GZIP压缩成功!");
return true;
}
//执行zip压缩命令
public boolean zipExec(String source, String to) {
System.out.println(source + " --> " +to + " ZIP压缩成功!");
return true;
}
}

代码清单32-16 解压缩接收者

1
2
3
4
5
6
7
8
9
10
11
12
public class UncompressReceiver implements IReceiver {
//执行gzip解压缩命令
public boolean gzipExec(String source, String to) {
System.out.println(source + " --> " +to + " GZIP解压缩成功!");
return true;
}
//执行zip解压缩命令
public boolean zipExec(String source, String to) {
System.out.println(source + " --> " +to + " ZIP解压缩成功!");
return true;
}
}

剩下的工作就是对抽象命令、具体命令稍作修改,这里不再赘述。为什么要在这里增加一个分支描述呢?这是为了与策略模式对比,在命令模式中,我们可以把接收者设计得与策略模式的算法相同,也可以不相同。我们按照职责设计的接口就不适用于策略模式,不可能封装一个叫做压缩的算法类,然后在类中提供两种不同格式的压缩功能,这违背了策略模式的意图——封装算法,为什么呢?如果要增加一个rar压缩算法,该怎么办呢?修改抽象算法?这是绝对不允许的!那为什么命令模式就是允许的呢?因为命令模式着重于请求者和接收者解耦,你管我接收者怎么变化,只要不影响请求者就成,这才是命令模式的意图。

命令、接收者都具备了,我们再来封装一个命令的调用者,如代码清单32-17所示。

代码清单32-17 调用者

1
2
3
4
5
6
7
8
9
10
11
public class Invoker {
//抽象命令的引用
private AbstractCmd cmd;
public Invoker(AbstractCmd _cmd){
this.cmd = _cmd;
}
//执行命令
public boolean execute(String source,String to){
return cmd.execute(source, to);
}
}

调用者非常简单,只负责把命令向后传递,当然这里也可以进行一定的拦截处理,我们暂时用不到就不做处理了。我们来看场景类是如何描述这个场景的,如代码清单32-18所示。

代码清单32-18 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) {
//定义一个命令,压缩一个文件
AbstractCmd cmd = new ZipCompressCmd();

/*
* 想换一个?执行解压命令
* AbstractCmd cmd = new ZipUncompressCmd();
*/
//定义调用者
Invoker invoker = new Invoker(cmd);
//我命令你对这个文件进行压缩
System.out.println("========执行压缩命令========");
invoker.execute("c:\\windows", "d:\\windows.zip");
}
}

想新增一个命令?当然没有问题,只要重新定义一个命令就成,命令改变了,高层模块只要调用它就成。请注意,这里的程序还有点欠缺,没有与文件的后缀名绑定,不应该出现使用zip压缩命令产生一个.gzip后缀的文件名,读者在实际应用中可以考虑与文件后缀名之间建立关联。

通过以上例子,我们看到命令模式也实现了文件的压缩、解压缩的功能,它的实现是关注了命令的封装,是请求者与执行者彻底分开,看看我们的程序,执行者根本就不用了解命令的具体执行者,它只要封装一个命令——“给我用zip格式压缩这个文件”就可以了,具体由谁来执行,则由调用者负责,如此设计后,就可以保证请求者和执行者之间可以相互独立, 各自发展而不相互影响。

同时,由于是一个命令模式,接收者的处理可以进行排队处理,在排队处理的过程中, 可以进行撤销处理,比如客人点了一个菜,厨师还没来得及做,那要撤回很简单,撤回也是命令,这是策略模式所不能实现的。

32.1.3 小结

策略模式和命令模式相似,特别是命令模式退化时,比如无接收者(接收者非常简单或者接收者是一个Java的基础操作,无需专门编写一个接收者),在这种情况下,命令模式和策略模式的类图完全一样,代码实现也比较类似,但是两者还是有区别的。

  • 关注点不同

策略模式关注的是算法替换的问题,一个新的算法投产,旧算法退休,或者提供多种算法由调用者自己选择使用,算法的自由更替是它实现的要点。换句话说,策略模式关注的是算法的完整性、封装性,只有具备了这两个条件才能保证其可以自由切换。

命令模式则关注的是解耦问题,如何让请求者和执行者解耦是它需要首先解决的,解耦的要求就是把请求的内容封装为一个一个的命令,由接收者执行。由于封装成了命令,就同时可以对命令进行多种处理,例如撤销、记录等。

  • 角色功能不同

在我们的例子中,策略模式中的抽象算法和具体算法与命令模式的接收者非常相似,但是它们的职责不同。策略模式中的具体算法是负责一个完整算法逻辑,它是不可再拆分的原子业务单元,一旦变更就是对算法整体的变更。

而命令模式则不同,它关注命令的实现,也就是功能的实现。例如我们在分支中也提到接收者的变更问题,它只影响到命令族的变更,对请求者没有任何影响,从这方面来说,接收者对命令负责,而与请求者无关。命令模式中的接收者只要符合六大设计原则,完全不用关心它是否完成了一个具体逻辑,它的影响范围也仅仅是抽象命令和具体命令,对它的修改不会扩散到模式外的模块。

当然,如果在命令模式中需要指定接收者,则需要考虑接收者的变化和封装,例如一个老顾客每次吃饭都点同一个厨师的饭菜,那就必须考虑接收者的抽象化问题。

  • 使用场景不同

策略模式适用于算法要求变换的场景,而命令模式适用于解耦两个有紧耦合关系的对象场合或者多命令多撤销的场景。