在上一篇文章里,我写了在阅读 ActiveMQ 的一小段源码时碰到的两种设计模式:抽象工厂和策略模式。实际上 ActiveMQ 源码量很大,只要认真分析,你会找到很多设计模式的应用场景。其中,有一个模式非常典型,并且它在整个 ActiveMQ 的源码架构中扮演者非常重要的角色,它就是 GoF 设计模式中的:命令模式(Command Pattern)。Command 是 ActiveMQ broker 与 client 的主要通信方式,每个 Command 对象代表了一个待执行的动作。关于命令模式在 ActiveMQ 源码中的应用,我会在后面找个时间专门写一下。
这篇文章讨论另一个问题。它是我在开始分析 ActiveMQ 中 Command 实现时碰到的。起因是这样的,我发现了一种不常见的接口定义:
/**
* Holds the command id constants used by the command objects.
*
* Mark by wxl24life: 2013-08-15
* Constant Interface Antipattern
*/
public interface CommandTypes {
// /
//
// Info objects sent back and forth client/server when
// setting up a client connection.
//
// /
byte WIREFORMAT_INFO = 1;
byte BROKER_INFO = 2;
byte CONNECTION_INFO = 3;
byte SESSION_INFO = 4;
byte CONSUMER_INFO = 5;
byte PRODUCER_INFO = 6;
byte TRANSACTION_INFO = 7;
byte DESTINATION_INFO = 8;
byte REMOVE_SUBSCRIPTION_INFO = 9;
// ...Other constants definement ignored
}
这种写法我是第一次碰到:接口中没有包含任何的方法,只是定义了一些常量属性。抱着这种疑惑,按照惯例自然首先要求助的便是 google,简单的两个关键字可以锁定到 SO 上的这个问答:What is the use of interface constants -- 小提示:在 google 搜索结果里,StackOverFlow 上的问答链接应该始终作为不二的选择。
从得票数最高的回答里获取到的最有价值信息是:这种用法称为常量接口,它是一种典型的 Java 反模式,并且在 Joshua Bloch 的 Effective Java 里有单独的条目详细描述了这个问题。所谓的反模式是指,不建议使用的方法。结合前面碰到的问题,CommandTypes 不应该或者至少是不建议被定义成那样的常量接口。换句话说,再优秀的开源代码中也会有一些不那么“优秀”的用法。真的是这样么?看下面的分析。
首先,接口中是可以定义属性的,在不带任何修饰符的情况下,默认是 static final 的。在接口中定义的属性可以在接口的实现类中直接访问,而不需要使用类名进行引用,也就是说,接口中的属性传递到了它的实现类的名字空间里去了。因此,在接口中已经定义了方法的前提下,偶尔定义少量的属性是可以接受的。但是,Java 同时允许常量接口的存在,所谓常量接口就是在接口中只定义属性而不包含方法,这显然违背了接口存在的初衷:接口为它的实现类提供了一种向外界暴露服务的方式。因而,当你定义常量接口的目的是为它的实现类访问常量属性提供便利的时候,就符合 Joshua Bloch 所说的 “常量接口是对接口的不良使用” 了。
在 Effective Java 第二版的条目19中描述了使用常量接口可能会产生的问题,以及它的替代用法。
- 类在内部使用某些常量属于实现细节,实现常量接口会导致这样的实现细节泄漏到该类的导出API。
- 如果非 final 类实现了常量接口,它的子类的名字空间也会被接口中的常量所污染。
通过一段代码来解释上面这两句话:
/**
* @author wxl24life
*
*/
public class AntiPatternConstantInterfaceImpl implements CommandTypes {
public byte getActivemqQueue() {
return ACTIVEMQ_QUEUE; // 常量接口带来的“便利”
}
public static void main(String[] args) {
AntiPatternConstantInterfaceImpl antiPat = new AntiPatternConstantInterfaceImpl();
byte a = antiPat.WIREFORMAT_INFO; // 产生的问题1:实现细节泄漏
new AntiPatternConstantInterfaceImpl() {
public void foo() {
byte b = WIREFORMAT_INFO; // 产生的问题2:子类名字空间被“污染”
}
};
}
}
当然,在 ActiveMQ 的源码里,CommandTypes 虽然被定义成常量接口,却没有任何类实现这个接口,常量接口在这里充当的角色是:定义一些公共的常量属性提供给其他类引用,相当于一个常量集合。这种用法严格上说不符合常量接口反模式的用法,但是使用接口定义一个常量集合仍然是不够合适的。
为此可以考虑另外的一些定义常量集合的方式,比如枚举类型(enum),或者定义一个不可实力化的工具类。
/**
* @author wxl24life
*
*/
public final class CommandTypesUtility {
private CommandTypesUtility() {}
public static final byte WIREFORMAT_INFO = 1;
public static final byte BROKER_INFO = 2;
public static final byte CONNECTION_INFO = 3;
public static final byte SESSION_INFO = 4;
public static final byte CONSUMER_INFO = 5;
public static final byte PRODUCER_INFO = 6;
public static final byte TRANSACTION_INFO = 7;
public static final byte DESTINATION_INFO = 8;
public static final byte REMOVE_SUBSCRIPTION_INFO = 9;
// other constants ignored
}
使用工具类定义常量后,当需要引用某个常量时只需要使用类似于 CommandTypesUtility.WIREFORMAT_INFO 的方式即可。而如果在同一个类里需要大量使用 CommandTypesUtility 中的常量,可以使用静态导入(static import)机制。
import static org.apache.activemq.command.CommandTypesUtility.*;
/**
* @author wxl24life
*
*/
public class StaticImportTest {
byte getWirformat_info() {
return WIREFORMAT_INFO;
}
byte getBroker_info() {
return BROKER_INFO;
}
// many more other uses
}