模式说明
工厂一词往往能够联想到生产某样产品,而在程序世界里生产的就是对象了,这个耳熟能详的设计模式,想要正确使用却不容易。工厂模式存在三种实现方式,分别是静态工厂(或叫简单工厂)、工厂方法、抽象工厂,每一种实现方式都存在它的应用场景。对于初学者而言,也比较容易混淆各种工厂模式的实现方式及应用场景,以至于错误的使用,甚至过度设计。
静态工厂
应用场景
静态工厂又名简单工厂,但我习惯称呼它为静态工厂,这是因为它通过静态方法返回对象的方式实现。它的应用场景主要用于替代构造方法,传统的创建对象方式是通过对象的构造方法并使用关键字 new 生产一个实例对象,这种传统的方式在代码层面上完全看不出代码作者的意图,而通过静态工厂创建对象则变得不同,以下是它的一些优点。
1. 有名有姓
现在我们以 ThreadPoolExecutor 作为构造方法创建一个单线程的线程池,具体的代码如下。
ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
很明显,仅通过上面的方式实例化一个对象根本就不知道作者的意图是什么,因此,我们只好去翻阅 ThreadPoolExecutor Javadoc 查看每一个参数的意义从而判断这里的 executor 作用。
再来用静态工厂实现,代码如下。
ExecutorService executor = Executors.newSingleThreadExecutor();
可以看出,对于代码的维护者来说,阅读这么一行代码,完全没有任何负担,通过字面的意思就能判断这是单线程执行器。
注意,这里只是一个例子,网络上存在一些不建议使用 Executors 的静态方法创建线程池相关对象的文章或规范,其中一个原因是因为其无限的任务列表容易导致 OOM 问题,而这里只是为了说明静态工厂的优点,因此,不需要过多的考虑。
2. 返回子类对象
假设现在我们需要根据不同的地区返回不同的 Calendar 实例,具体的代码逻辑如下。
Locale locale = ...;
if (locale.getLanguage() == "th" && locale.getCountry() == "TH") {
// th_TH 台湾
cal = new BuddhistCalendar(zone, locale);
} else if (locale.getVariant() == "JP" && locale.getLanguage() == "ja"
&& locale.getCountry() == "JP") {
// ja_JP_JP 日本
cal = new JapaneseImperialCalendar(zone, locale);
} else {
// others 其他
cal = new GregorianCalendar(zone, locale);
}
BuddhistCalendar / JapaneseImperialCalendar / GregorianCalendar 都是 Calendar 的子类,如果每一次创建对象都需要这种策略,那么 Calendar 就可以提供静态工厂方法根据不同的地区创建相应的子类对象了,这正是 java.util.Calendar.getInstance() 的实现逻辑,代码如下。
public abstract class Calendar implements ... {
public static Calendar getInstance() {
return createCalendar(TimeZone.getDefault(),
Locale.getDefault(Locale.Category.FORMAT));
}
}
即使后续 Calendar 增加子类也不需要变更上层的代码,兼容性也因而提高。
3. 返回单例对象
静态工厂可以不用每次返回新对象,或单例和多例之间自由切换,Java 中 Collections.emptyList() 就是一个返回单例对象的静态工厂方法。
4. 参数控制返回对象
以 EnumSet 为例,EnumSet.of(E, …E) 则会根据参数数量来判断使用 RegularEnumSet 还是 JumboEnumSet,当 enum 个数小于等于 64 的时候使用 RegularEnumSet,否则使用 JumboEnumSet。
为什么以 64 为分割线?这是因为一个 long 类型的位数就是 64,对于 RegularEnumSet,则是根据 enum 对象的 ordinal 值确定所在的 bit 位置。同样的,JumboEnumSet 也是根据 enum 的 ordinal 值确定具体位置,但由于 enum 的个数已经超出 long 的最大位数(64),那么取而代之的是 long 数组。
5. 对版本控制友好
静态工厂对 git / svn 的版本控制友好,如果以 new 的方式新建对象,改动时必然会牵一发而动全身,整个代码库中所有引用的地方都需要调整,无疑增加了 status file 的数量,对于复查人员或从历史排查角度来说感觉极差。
6. 常见的静态工厂命名
清楚了静态工厂的优点后,再来看看,静态工厂方法常见的命名方式。
方法名 | 说明 | 举例 |
---|---|---|
from | 从参数对象中以特定算法转换为新对象 | Date date = java.util.Date.from(Instant) GregorianCalendar calendar = GregorianCalendar.from(ZonedDateTime) |
of | 以聚合一个或多个参数的形式返回新对象 | EnumSet enumSet = EnumSet.of(E, E…) Stream stream = Stream.of(T…) |
valueOf | 类似于 of,但提供了更详细的说明,同理于 serviceOf / arrayOf | BitSet bitSet = BitSet.valueOf(long[]) |
instance getInstance |
获取当前类的实例对象 | Calendar calendar = Calendar.getInstance() |
get{Type} | 与 getInstance 类似,但更加明确返回的类型,有时候 {Type} 是可选项 | Path path = Paths.get(URI) FileStore fileStore = Files.getFileStore(Path) |
new{Type} | 创建一个新对象,但一般与 {Type} 一起使用,如果 {Type} 无法很好把握它的命名,可以命名为 newInstance | DirectoryStream Object array = Array.newInstance(Class<?>, int) InputStream in = Files.newInputStream(Path, OpenOptin…) |
create | 与 newInstance / getInstance 类似 | URI uri = URI.create(String) Instant instant = java.time.Instant.create(long, long) |
实现方式
根据不同的参数值类型创建对应的 Condition 对象,为明确 Value 的类型,可通过方法名满足,如下代码所示。
public class Condition {
private String name;
private Object value;
public Condition(String name, Object value) {
this.name = name;
this.value = value;
}
public static Condition newNullValue(String name) {
return new Condition(name, null);
}
public static Condition newStringValue(String name, String strValue) {
return new Condition(name, strValue);
}
public static Condition newIntegerValue(String name, Integer intValue) {
return new Condition(name, intValue);
}
}
// 使用方式
Condition nullValue = Condition.newNullValue("address");
Condition stringValue = Condition.newStringValue("name", "Edward");
Condition integerValue = Condition.newIntegerValue("age", 19);
工厂方法
应用场景
对于面向对象的程序而言,到处都会涉及到对象的创建,而工厂方法又是创建或提供对象的地方,因此非常容易被滥用。工厂方法是向客户端程序提供以某种策略创建对象的方法,就如同 java.util.concurrent.ThreadFactory 的作用,给客户端程序提供一种创建线程的可控策略。
java.util.concurrent.ThreadFactory 是一个在线程池里创建工作线程或者任务的工厂,该接口内只包含一个 newThread 方法生成线程池里的工作线程或任务
另外一种,是解决参数蔓延的问题,例如为了响应 JSON 数据,我们需要在最顶层对象中传入 JSON 相关格式要求的参数或配置,再把该参数或配置传入 JSON 序列化工具(对象转为 JSON 字符串),流程如下图 1 所示。那么有什么参数或配置是 JSON 序列化的时候需要的呢?例如是否响应一行或格式化后的多行,统一使用单引号或是双引号,日期的格式等等。
图 1
很显然,HttpResponse 其实并不关心 options 参数,但又因为 options 参数把 HttpResponse 和 JSONSerializer 耦合在一起,如果后期需要响应 XML / YML 格式的数据,那么 options 还会变得十分臃肿。这个时候我们可以使用工厂方法来解耦 HttpReponse 和 JSONSerializer,引入工厂方法后的关系,如下图 2 所示。
图 2
这里仅限响应 JSON 格式的数据,如果需要响应更多类型的数据格式则 JSONFactory 需要以接口形式入参,而 java.util.concurrent.ThreadFactory 相当于也解决了参数蔓延的问题。
所以,工厂方法解决的是以下程序设计痛点,只有清楚的意识到设计模式要解决的问题,才能用得游刃有余
- 让客户端程序以某种策略创建对象
- 创建对象的参数蔓延
- (期待补充更多)
实现方式
根据应用场景,再来一段简单的实现,主要是实现 HttpResponse / JSONFactory,具体代码如下。
public class HttpResponse {
private Object msgBody;
private JSONFactory factory;
public HttpResponse(JSONFactory factory, Object msgBody) {
this.factory = factory;
this.msgBody = msgBody;
}
public String getMessageBody() {
return result == null ? "" : factory.serialize(msgBody);
}
}
public class JSONFactory {
private boolean doubleQuoted;
private String dateFormat;
public JSONFactory() { }
public String serialize(Object obj) {
// 使用不同的 JSON 库
// 根据 doubleQuoted / dateFormat 配置返回不同的 JSON 字符串
return /* gson / fastjson / jackson *//* toJson */;
}
public void setDoubleQuoted(boolean doubleQuoted) {
this.doubleQuoted = doubleQuoted;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
}
具体使用方式如下代码所示。
Map<String, Object> returnAttrs = new HashMap<>();
JSONFactory factory = new JSONFactory();
factory.setDoubleQuoted(true);
factory.setDateFormat("YYYY-MM-dd HH:mm:ss");
HttpResponse response = new HttpResponse(factory, returnAttrs);
String httpMessageBody = response.getMessageBody();
抽象工厂
应用场景
抽象工厂一般可以生产多种对象,而所生产的对象则属于同一产品族,我们不妨在工厂方法基础上增加一个需求,不仅响应的数据是 JSON 格式,而接收的数据也必须是 JSON 格式(GET / HEAD 方法除外),换句话说,就是请求和响应都只能是 JSON 格式的数据,具体流程如下图 3 所示。
图 3
除了 JSON 格式,我们还要对请求及响应支持 XML / YML 格式的数据,基于这个背景下,我们不能让 JSON 格式的请求响应 XML / YML 格式的数据,这不仅脱离了需求,还会使得调用接口的开发者非常懊恼。由于 JSON / XML / YML 分别属于各自的产品族,因此,在这里我们就可以引入抽象工厂,具体流程如下图 4 所示。
图 4
实现方式
抽象工厂涉及到的类比较多,因此只给出类名和方法名的代码片段,具体代码如下。
public abstract class AbstractFactory {
public abstract <T> T deserialize(String str, Class<T> clazz);
public abstract String serialize(Object obj);
}
其实,AbstractFactory 也可以是接口,这里为了突出是抽象这个词,才特意声明为抽象类。
public class XMLFactory extends AbstractFactory {
public <T> T deserialize(String xml, Class<T> clazz) {
return /* toObject(xml, clazz) */;
}
public String serialize(Object obj) {
return /* toXMLString(obj) */;
}
}
public class JSONFactory extends AbstractFactory {
public <T> T deserialize(String json, Class<T> clazz) {
return /* toObject(json, clazz) */;
}
public String serialize(Object obj) {
return /* toJSONString(obj) */;
}
}
public class YMLFactory extends AbstractFactory {
public <T> T deserialize(String yml, Class<T> clazz) {
return /* toObject(yml, clazz) */;
}
public String serialize(Object obj) {
return /* toYMLString(obj) */;
}
}
现在 AbstractFactory 已经不适合仅仅作为 HttpResponse 参数了,而应该作为整个请求及响应的上下文 (假设请求下上文的类名为 ServerContext)的参数。
public class ServerContext {
public AbstractFactory factory;
public ServerContext(AbstractFactory factory) {
this.factory = factory;
}
public <T> T getRequest(String req, Class<T> clazz) {
return factory.deserilize(req, clazz);
}
public String getResponse(Object obj) {
return factory.serilize(obj);
}
}