本文是软件构造系列的第六篇文章。其余文章在:
https://ruanx.net/tag/software-construction/

什么是工厂方法

  众所周知,对象的方法大致可以分为 Creator、Producer、Observer、Mutator 四类。其中的 Creator 是创建一个新的实例,静态 Creator 方法一般称为工厂方法。

  对于单个类而言,采用工厂方法而不是 new MyClass() 的好处有很多。首先,可以使得代码的语义变得更加通顺,例如 Guava 的数据结构中,提供了 of 这个工厂方法:

Set<String> s = ImmutableSet.of("hello", "www", "blue");

  显然,这比起 new ImmutableSet("hello", "www", "blue") 更加容易理解。此外,一个类可以有很多个工厂方法,仍以 Set 为例:我们可以直接在参数中指定几个元素,可以从文件中读取元素,可以从数据库中获取元素……这些可以利用工厂方法,实现为 MySet.of() , MySet.fromFile(), Myset.fromDatabase() . 比起 new 一个集合并手动插入元素,显著节省了 client 的工作量。

  另外,工厂方法为单例模式提供了支持。假设我们有一个类,希望所有的客户端都采用同一个实例,也就是说,不允许各个客户端 new 一个实例出来。那我们可以这样写:

class MyClass {
    private static MyClass obj = null;
    
    private MyClass() {
        super();
    }
    
    public MyClass getInstance() {
        if (obj == null) {
            obj = new MyClass();
            return obj;
        }
        
        return obj;
    }
}
▲ 工厂方法实现单例模式

  工厂方法是好的,不过我们刚刚的讨论仅限于一个单独的类。如果有很多个产品需要创建实例,我们可以实现一个大工厂,让用户指定需要的产品,返回对应产品的实例。这种模式实现了“创建与使用相分离”,称为简单工厂模式。

  下面来看一个例子。今有两种笔(分别记为 A, B)可以生产,定义如下:

interface Pen {
    void show();
}

class PenA implements Pen{
    public void show() {
        System.out.println("I am Pen A.");
    }
}

class PenB implements Pen{
    public void show() {
        System.out.println("I am Pen B.");
    }
}

  接下来,为了统一接口,让用户只需要利用单独的工厂方法就能制造任何一种笔,我们用一个新的类来完成“生产”:

class Factory {
    public static Pen getPen(String target) {
        if ("A".equals(target))
            return new PenA();
        else
            return new PenB();
    }
}


public class SimpleFactory {
    public static void main(String[] args) {
        Factory.getPen("A").show();
    }
}

  于是创建和使用得以分离。上面这个 Factory 不仅采用了简单工厂模式,还采用了 facade 模式。

  现在,我们有了新的问题。假设我们要添加一个产品 C,那么为了允许 client 创建一个 C 产品,我们必须修改 Factory 类。这违反了开闭原则

  为了在符合开闭原则的情况下,保留工厂方法“client无需知道具体产品的规约,只需要知道工厂方法的规约”这个优点,我们改用工厂方法模式。

工厂方法模式

  工厂方法模式对上述问题的解决方案是单一责任。提供很多个工厂类,每个工厂类只负责生产一个产品。当增加产品时,显然只需要增加一个工厂类,无需更改其他工厂、无需更改 client 代码。

  看一个例子。 Pen 沿用上述代码,我们创建多个工厂:

interface PenFactory {
    public Pen createPen();
}

class CreatorA implements PenFactory {
    public Pen createPen() {
        return new PenA();
    }
}

class CreatorB implements PenFactory {
    public Pen createPen() {
        return new PenB();
    }
}

public class FactoryPattern {
    public static void main(String[] args) {
        PenFactory fac = new CreatorA();
        fac.createPen().show();
    }
}

  这样,客户端想要产品 A,就用 CreatorA 来生产一个;想要别的,就用其他工厂类来生产。于是达到了“对扩展开放、对修改封闭”的原则。

  但是,这种模式比起给每个产品类添加自己的工厂方法,到底好在哪里?

  • 接口隔离。客户端不需要知道各个产品的 class 名称,只需要知道对应工厂的名称。有一些复杂系统,产品名称非常冗长,是内部名称,不方便暴露给用户。
  • 单一责任。创建与使用分离得很开,这对于创建产品时的额外工作时有利的。例如,假设开发者想对某个类搞单例模式,将这种代码写进对应类里面的静态工厂方法是不太合适的——类里面只应该写那个类该干的事情,而“这个类全局只能有一个”是外部“环境”该考虑的事情,不是这个类本身的责任,所以写在这个类外面更加适当。
  • 路由。工厂方法模式并不是固定的“一个产品类对应一个工厂类”;相反地,可以多个工厂类对应一个产品类,甚至一个产品类不对应任何工厂(弃用)。举一个例子,假设 A 产品产自中国,B 产品产自欧盟,我们的 client 需要一支“中国笔”,侧重的是“产自中国”这个要素,而不关心它的编号是 A 还是 B. 此时,可以用下面这个工厂来满足客户需求:
class CreateMadeInChinaPen implements PenFactory {
    public Pen createPen() {
        return new PenA();
    }
}

  于是对 client 封闭了自己的实现细节,client 只需要关注自己想要的产品特征,把这些特征交由我们的工厂实现即可。

  另外,工厂模式可以轻微地违背开闭原则,来提供“更新”功能。比如,现在 A 笔更贵,所以我们实现一个工厂:

class CreateExpensivePen implements PenFactory {
    public Pen createPen() {
        return new PenA();
    }
}

  客户端如果想要一支最贵的笔,只需要调用 CreateExpensivePen 这个工厂。如果我们又研制出了 C 笔,这支笔售价比 A 还贵,我们可以把上面的实现改为:

class CreateExpensivePen implements PenFactory {
    public Pen createPen() {
        return new PenC();
    }
}

  版本升级后,client 取得的最贵的笔变成了 C,也就是更新了。与此同时,client 自己的代码是无需修改的!我们轻微地修改了工厂,同时保证了 client 对修改封闭,只需要客户升级库版本,就能得到最新的正确的 ExpensivePen .

  最后,工厂方法模式与简单工厂模式相比,Creator 从静态方法变成了普通方法。这样做的理由是,java 中静态方法不继承,所以我们改用普通方法,以便于继承“父产品”的特征,更好地做到代码复用。

抽象工厂模式

  现在我们可以利用工厂方法模式来创建单一产品了。抽象工厂模式在此基础上更进一步,提供了创建“产品族”的接口。

  何谓“产品族”也?我们知道,各个厂商的笔记本电脑与手机适配得比较好。例如小米笔记本用户有很多是小米手机用户、苹果电脑用户有很多都是苹果手机用户。我们将这种一致性抽象出来:

  • 一个产品族,是指多种产品的组合。
  • 如果采用了某个产品族, 则所有产品都得使用这个产品族的。

  例如,以下表格的列是“产品”,行是“产品族”:

手机 电脑
小米产品族 小米9 Mi Air
苹果产品族 iphone8 MacBook

  一个人可以用小米9+Mi Air,但不可以用 iphone8 + Mi Air. 这就是产品族的意义:选择了小米,就选择了它的全套解决方案。

  现在,假设一个客户想创建小米的手机和小米的电脑,但不知道这些产品的具体型号。也就是说,客户关注的是“品牌”,而非产品本身的标签。要客户去分别调用小米9、Mi Air 的工厂方法显然是不合适的,因为客户不知道这些单个产品的接口。抽象工厂模式应运而生——它为用户提供了“创建一系列产品”的功能。

  首先,我们定义上面这四种产品:

interface Phone {
    public void show();
}

class Mi9 implements Phone {
    public void show() {
        System.out.println("I am phone - Mi 9.");
    }
}

class iphone8 implements Phone {
    public void show() {
        System.out.println("I am phone - iphone 8.");
    }
}

interface Laptop {
    public void show();
}

class MiAir implements Laptop {
    public void show() {
        System.out.println("I am laptop - Mi air.");
    }
}

class MacBook implements Laptop {
    public void show() {
        System.out.println("I am laptop - MacBook.");
    }
}

  接下来,定义抽象工厂的接口:

interface BrandFactory {
    Phone createPhone();
    Laptop createLaptop();
}

  一个抽象工厂可以产出手机和平板电脑,也可以仅产出一个或者根本不产出。所以提供了 createPhonecreateLaptop 两个接口,让客户按需调用。

  小米工厂产出的是 Mi 9Mi Air 产品,苹果工厂产出的是 iphone8MacBook 产品。工厂实现如下:

class MiFactory implements BrandFactory {
    public Phone createPhone() {
        return new Mi9();
    }
    public Laptop createLaptop() {
        return new MiAir();
    }
}

class AppleFactory implements BrandFactory {
    public Phone createPhone() {
        return new iphone8();
    }
    public Laptop createLaptop() {
        return new MacBook();
    }
}

  最后,client 只需要选择一个品牌,调用其抽象工厂的接口,即可创建出自己想要的产品,全程无需知道产品的名字等细节。

public class AbstractFactory {
    public static void main(String[] args) {
        BrandFactory fac = new MiFactory();
        fac.createLaptop().show();   // Mi Air
        fac.createPhone().show();    // Mi 9
    }
}

  讨论一下抽象工厂的开闭原则问题。我们首先要有一个 big picture:抽象工厂模式,就像关系型数据库里的一张名为“解决方案”的表。每一列对应一种产品种类(例如手机、电脑);每一行对应一种解决方案(例如小米方案、苹果方案),也就是一个产品族。

  新增一个产品族,在这张表上体现为新增一行记录,这是非常方便的,无需改动表的其他部分。这方面是完美地符合开闭原则。

  要改动一个产品族的某个产品(例如把小米方案的手机改成 Mi 10),相当于在表上修改一条记录。只需要改动小米方案的抽象工厂,无需改动 client 代码。这里轻微地违反了开闭原则,但客户端无需改动。

  但如果想要引入某个产品种类(例如,引入电视机产品),那就麻烦了——这个任务相当于在表上添加一个列,是要改动所有行的。每一个产品族,都得为电视机产品添加创建策略,所以引入新产品种类是严重违背开闭原则的。不过,引入电视机之前的 client 代码仍然无需改动,因为它们无需使用电视。

  最后需要提一句,抽象工厂模式没有限制“一个产品只能被一个产品族包含”。例如,有些产品小米根本没有,这种情况下抽象工厂可以返回其他厂商的对应产品。抽象工厂也可以实现“最佳推荐单”——每个产品取自不同的厂商,最后组合成一个产品族,一起发挥效用。通过继承、decorator 模式等操作,还可以实现不同业务场景的组合——例如“小米的电子设备解决方案+Jeep的出行解决方案+晨光的办公文具解决方案”。

  抽象工厂是很灵活的设计模式,但我认为其核心思想是不变的:“减少用户的选择余地”。本来用户的选择方案空间,是各种产品集合的笛卡尔积;抽象工厂模式大大降低了组合数量,仅提供几套工作较好的选择方案给用户。这可以避免用户在不知道各种产品细节的情况下作出较坏决策,也向用户隐藏了自己的内部实现。