模式说明

工厂一词往往能够联想到生产某样产品,而在程序世界里生产的就是对象了,这个耳熟能详的设计模式,想要正确使用却不容易。工厂模式存在三种实现方式,分别是静态工厂(或叫简单工厂)、工厂方法、抽象工厂,每一种实现方式都存在它的应用场景。对于初学者而言,也比较容易混淆各种工厂模式的实现方式及应用场景,以至于错误的使用,甚至过度设计。

静态工厂

应用场景

静态工厂又名简单工厂,但我习惯称呼它为静态工厂,这是因为它通过静态方法返回对象的方式实现。它的应用场景主要用于替代构造方法,传统的创建对象方式是通过对象的构造方法并使用关键字 new 生产一个实例对象,这种传统的方式在代码层面上完全看不出代码作者的意图,而通过静态工厂创建对象则变得不同,以下是它的一些优点。

1. 有名有姓

现在我们以 ThreadPoolExecutor 作为构造方法创建一个单线程的线程池,具体的代码如下。

  1. ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L,
  2. TimeUnit.MILLISECONDS,
  3. new LinkedBlockingQueue<Runnable>());

很明显,仅通过上面的方式实例化一个对象根本就不知道作者的意图是什么,因此,我们只好去翻阅 ThreadPoolExecutor Javadoc 查看每一个参数的意义从而判断这里的 executor 作用。

再来用静态工厂实现,代码如下。

  1. ExecutorService executor = Executors.newSingleThreadExecutor();

可以看出,对于代码的维护者来说,阅读这么一行代码,完全没有任何负担,通过字面的意思就能判断这是单线程执行器。

注意,这里只是一个例子,网络上存在一些不建议使用 Executors 的静态方法创建线程池相关对象的文章或规范,其中一个原因是因为其无限的任务列表容易导致 OOM 问题,而这里只是为了说明静态工厂的优点,因此,不需要过多的考虑。

2. 返回子类对象

假设现在我们需要根据不同的地区返回不同的 Calendar 实例,具体的代码逻辑如下。

  1. Locale locale = ...;
  2. if (locale.getLanguage() == "th" && locale.getCountry() == "TH") {
  3. // th_TH 台湾
  4. cal = new BuddhistCalendar(zone, locale);
  5. } else if (locale.getVariant() == "JP" && locale.getLanguage() == "ja"
  6. && locale.getCountry() == "JP") {
  7. // ja_JP_JP 日本
  8. cal = new JapaneseImperialCalendar(zone, locale);
  9. } else {
  10. // others 其他
  11. cal = new GregorianCalendar(zone, locale);
  12. }

BuddhistCalendar / JapaneseImperialCalendar / GregorianCalendar 都是 Calendar 的子类,如果每一次创建对象都需要这种策略,那么 Calendar 就可以提供静态工厂方法根据不同的地区创建相应的子类对象了,这正是 java.util.Calendar.getInstance() 的实现逻辑,代码如下。

  1. public abstract class Calendar implements ... {
  2. public static Calendar getInstance() {
  3. return createCalendar(TimeZone.getDefault(),
  4. Locale.getDefault(Locale.Category.FORMAT));
  5. }
  6. }

即使后续 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 dirStream = Files.newDirectoryStream(Path)

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 的类型,可通过方法名满足,如下代码所示。

  1. public class Condition {
  2. private String name;
  3. private Object value;
  4. public Condition(String name, Object value) {
  5. this.name = name;
  6. this.value = value;
  7. }
  8. public static Condition newNullValue(String name) {
  9. return new Condition(name, null);
  10. }
  11. public static Condition newStringValue(String name, String strValue) {
  12. return new Condition(name, strValue);
  13. }
  14. public static Condition newIntegerValue(String name, Integer intValue) {
  15. return new Condition(name, intValue);
  16. }
  17. }
  18. // 使用方式
  19. Condition nullValue = Condition.newNullValue("address");
  20. Condition stringValue = Condition.newStringValue("name", "Edward");
  21. Condition integerValue = Condition.newIntegerValue("age", 19);

工厂方法

应用场景

对于面向对象的程序而言,到处都会涉及到对象的创建,而工厂方法又是创建或提供对象的地方,因此非常容易被滥用。工厂方法是向客户端程序提供以某种策略创建对象的方法,就如同 java.util.concurrent.ThreadFactory 的作用,给客户端程序提供一种创建线程的可控策略。

java.util.concurrent.ThreadFactory 是一个在线程池里创建工作线程或者任务的工厂,该接口内只包含一个 newThread 方法生成线程池里的工作线程或任务


另外一种,是解决参数蔓延的问题,例如为了响应 JSON 数据,我们需要在最顶层对象中传入 JSON 相关格式要求的参数或配置,再把该参数或配置传入 JSON 序列化工具(对象转为 JSON 字符串),流程如下
图 1 所示。那么有什么参数或配置是 JSON 序列化的时候需要的呢?例如是否响应一行或格式化后的多行,统一使用单引号或是双引号,日期的格式等等。
image.png
图 1

很显然,HttpResponse 其实并不关心 options 参数,但又因为 options 参数把 HttpResponse 和 JSONSerializer 耦合在一起,如果后期需要响应 XML / YML 格式的数据,那么 options 还会变得十分臃肿。这个时候我们可以使用工厂方法来解耦 HttpReponse 和 JSONSerializer,引入工厂方法后的关系,如下图 2 所示。
image.png
图 2

这里仅限响应 JSON 格式的数据,如果需要响应更多类型的数据格式则 JSONFactory 需要以接口形式入参,而 java.util.concurrent.ThreadFactory 相当于也解决了参数蔓延的问题。

所以,工厂方法解决的是以下程序设计痛点,只有清楚的意识到设计模式要解决的问题,才能用得游刃有余

  1. 让客户端程序以某种策略创建对象
  2. 创建对象的参数蔓延
  3. (期待补充更多)

实现方式

根据应用场景,再来一段简单的实现,主要是实现 HttpResponse / JSONFactory,具体代码如下。

  1. public class HttpResponse {
  2. private Object msgBody;
  3. private JSONFactory factory;
  4. public HttpResponse(JSONFactory factory, Object msgBody) {
  5. this.factory = factory;
  6. this.msgBody = msgBody;
  7. }
  8. public String getMessageBody() {
  9. return result == null ? "" : factory.serialize(msgBody);
  10. }
  11. }
  12. public class JSONFactory {
  13. private boolean doubleQuoted;
  14. private String dateFormat;
  15. public JSONFactory() { }
  16. public String serialize(Object obj) {
  17. // 使用不同的 JSON 库
  18. // 根据 doubleQuoted / dateFormat 配置返回不同的 JSON 字符串
  19. return /* gson / fastjson / jackson *//* toJson */;
  20. }
  21. public void setDoubleQuoted(boolean doubleQuoted) {
  22. this.doubleQuoted = doubleQuoted;
  23. }
  24. public void setDateFormat(String dateFormat) {
  25. this.dateFormat = dateFormat;
  26. }
  27. }

具体使用方式如下代码所示。

  1. Map<String, Object> returnAttrs = new HashMap<>();
  2. JSONFactory factory = new JSONFactory();
  3. factory.setDoubleQuoted(true);
  4. factory.setDateFormat("YYYY-MM-dd HH:mm:ss");
  5. HttpResponse response = new HttpResponse(factory, returnAttrs);
  6. String httpMessageBody = response.getMessageBody();

抽象工厂

应用场景

抽象工厂一般可以生产多种对象,而所生产的对象则属于同一产品族,我们不妨在工厂方法基础上增加一个需求,不仅响应的数据是 JSON 格式,而接收的数据也必须是 JSON 格式(GET / HEAD 方法除外),换句话说,就是请求和响应都只能是 JSON 格式的数据,具体流程如下图 3 所示。
image.png
图 3

除了 JSON 格式,我们还要对请求及响应支持 XML / YML 格式的数据,基于这个背景下,我们不能让 JSON 格式的请求响应 XML / YML 格式的数据,这不仅脱离了需求,还会使得调用接口的开发者非常懊恼。由于 JSON / XML / YML 分别属于各自的产品族,因此,在这里我们就可以引入抽象工厂,具体流程如下
图 4 所示。
image.png
图 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);
    }
}