Cover Image

现今在互联网圈,设计模式并不是那么容易被提起了,这恐怕和互联网的快速迭代和微服务的流行有一些关系。设计模式在函数式编程火热的时候还一度成为了 anti-pattern,被大家觉得古板、无用。当然,在这股函数式编程热潮中中枪的可能是面向对象编程,设计模式是作为面向对象编程中的精华而遭到了唾弃。

实际上再回过头想一下,这些设计模式是实际开发中提炼出的有用经验,在一定程度上确实可以提高程序的可读性和可扩展性;另一方面,设计模式作为固定的套路,也加深了代码在作者和读者之间理解的程度,在这个意义上,它可以作为一种沟通的媒介。当然,前提是在正确的场景下使用。所谓正确的场景,就是合适的场景,并非为了使用而生搬硬套,而是确实解决了某些问题。

这篇文章里,我来简单介绍一下一种最常见的设计模式:工厂模式,谈谈我对这个模式的理解。

介绍

工厂模式可能是 Java 中使用最多的设计模式之一。它属于对象创建模式。在工厂模式中,我们通过工厂来创建对象,隐藏创建对象的逻辑,并使用公共的接口将这个对象暴露给客户端。

我们先来看最简单的一种工厂模式。

简单工厂

绝大部分日常代码工作中,简单工厂就足够我们使用。它适用于我们想通过一个工厂类来封装某一个产品类别(即想创建实例的类)的创建逻辑时。且这个产品类有可能有多种子类型,但是调用方并不关心具体是哪种类型,而是希望通过传递一定的参数信息,来告知工厂来选择合适的子类型并创建。

下面举个实际的例子。

假设我们有一个产品类别叫 Car,代表汽车。每个汽车厂商都能生产不同品牌的 Car,我们把这些不同品牌的 Car 看作 Car 的子类。这样,我们可以有 BmwCarVolvoCarTeslaCar 等等,如下图所示。

简单工厂模式示例

假设有一个神奇的万能造车工厂,叫做 CarFactory,它可以生产多种品牌的汽车,也就是说既可以生产 BmwCar 还能生产 VolvoCarTeslaCar 。当我们想生产某种 Car 时,我只需要告诉这个神奇的工厂 CarFactory,我想要什么牌子的汽车,它就能负责把它生产出来。我不需要关心它具体是怎么生产的,它最后只需要交付给我一个满足需求的 Car 即可。

我们作为这个工厂的客户,该如何告诉 CarFactory 生产什么牌子的汽车呢?可以通过参数传递给 CarFactorycreateCar() 方法。例如:

1
2
3
4
5
6
7
8
Car car1 = SimpleCarFactory.createCar(CarType.BMW);
car1.drive();

Car car2 = SimpleCarFactory.createCar(CarType.VOLVO);
car2.drive();

Car car3 = SimpleCarFactory.createCar(CarType.TESLA);
car3.drive();

createCar() 方法中,可以根据传入的参数进行选择最终需要生产的类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class SimpleCarFactory {

  public static Car createCar(CarType type) {
    if (type == null) {
      throw new IllegalArgumentException("type cannot be null");
    }
    if (type == CarType.BMW) {
      return new BmwCar();
    } else if (type == CarType.VOLVO) {
      return new VolvoCar();
    } else if (type == CarType.TESLA) {
      return new TeslaCar();
    } else {
      throw new IllegalArgumentException("Unknown car type: " + type);
    }
  }
}

完整代码示例,请参考代码仓库:https://github.com/gdong42/factory-pattern

优势和缺点

这种模式中,调用方不用关心所需要的实际类型是什么,只需提供相应的类型信息即可。这种方式方便了调用方,将调用的客户端逻辑和具体实现类的构造逻辑在一定程度上解耦了开来。

那么这种模式有什么缺陷呢?从实际出发考虑,现实生活中很少有这种超级工厂,原因是这个工厂需要了解所有具体的产品的生产方式,不太实际。面向对象的软件工程反映实际世界,代码中也是一样,这个工厂类拥有所有产品的生产逻辑。每当我们需要增加一种产品类型时,都需要修改 createCar 方法。这显然不太理想。

另外,SimpleCarFactory 这个工厂类因为需要包含所有 Car 的生产逻辑,它必须依赖所有的类型。这导致客户端代码耦合所有的可选产品类型,而这些产品类型的依赖调用端可能永远也不需要使用。假设我们将各种具体产品类型和它们相关的实现类分别打包成为独立模块,则这个超级工厂需要依赖所有的模块。而我们希望的是,当我们生产某种特定的产品类型时,提供相应的实现的模块依赖即可。

现实中的例子

在 Java 中,String.valueOf() 系列方法是简单工厂的一个不错的例子。

String 类在 Java 中代表字符串。有时,我们需要从布尔或是整数类型的变量中获得它们的字符串表示。String 并没有提供像是 String(Integer i)String(Boolean b) 这样的构造器,而是提供了多种简单工厂方法 String.valueOf(…)

1
2
int i = 12;
String integerAsString = String.valueOf(i);

更上一层楼

如何基于这种方式改进解决这些问题呢?

这里介绍规避上述问题可能有用的两种方式。

  1. 类注册 - 使用一个映射表维护具体的类型和类型名称之间的关系。这种方式又可以使用以下两种途径实现:
    • 使用反射
    • 不使用反射
  2. 工厂方法模式 - GoF 设计模式的书中提到的著名的工厂方法模式

我们接下来稍微展开描述一下具体细节。

类注册

类型注册的过程可以通过调用工厂的注册方法来实现,而工厂具体的制造具体类实例的逻辑可以使用两种方式进行:使用反射的方式和不使用反射的方式。使用类注册的方式,我们可以在工厂的实现中维护一张从类型名到具体类实例的映射表。为了可以扩展系统可以接受更多的类型,我们还需要提供一个方法用来接受新的类型的注册。这样一来,增加新的产品类型不再需要修改工厂类中的任何代码。

使用反射的类注册方式

当工厂需要制造新的类实例的时候,它首先从内部维护的请求类型名称到类型的映射表中查询得到具体的类型,再通过反射调用该类的构造器调用实际的创造逻辑。这个过程可以通过下面的代码演示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static class CarFactoryWithReflection {

  private Map<CarType, Class<? extends Car>> registeredCarTypes = new ConcurrentHashMap<>();

  private static CarFactoryWithReflection INSTANCE;

  private CarFactoryWithReflection() {
  }

  public static CarFactoryWithReflection instance() {
    if (INSTANCE == null) {
      INSTANCE = new CarFactoryWithReflection();
    }
    return INSTANCE;
  }

  public void registerCar(CarType type, Class<? extends Car> carClass) {
    this.registeredCarTypes.put(type, carClass);
  }

  public Car createCar(CarType type) {
    if (type == null) {
      throw new IllegalArgumentException("type cannot be null");
    }
    Class<? extends Car> carClass = registeredCarTypes.get(type);
    if (carClass == null) {
      throw new RuntimeException("Car type not registered: " + type);
    }
    try {
      Constructor<? extends Car> carConstructor = carClass
          .getDeclaredConstructor();
      return carConstructor.newInstance();
    } catch (Exception e) {
      throw new RuntimeException(
          "Car of type " + type + " cannot be instantiated", e);
    }
  }
}

这种方法需要在调用 createCar() 方法前在工厂中注册这种类型。可以通过在客户端代码中显式调用工厂类的 registerCar 方法实现。例如,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    public static void main(String[] args) {

      CarFactoryWithReflection.instance()
          .registerCar(CarType.BMW, BmwCar.class);
      CarFactoryWithReflection.instance()
          .registerCar(CarType.VOLVO, VolvoCar.class);
      CarFactoryWithReflection.instance()
          .registerCar(CarType.TESLA, TeslaCar.class);

      Car car1 = CarFactoryWithReflection.instance().createCar(CarType.BMW);
      car1.drive();

      Car car2 = CarFactoryWithReflection.instance().createCar(CarType.VOLVO);
      car2.drive();

      Car car3 = CarFactoryWithReflection.instance().createCar(CarType.TESLA);
      car3.drive();
    }

这种反射的实现方式有一些缺点。

  • 性能 - 使用反射所带来的性能损耗
  • 不能处理当类的构造器有比较复杂的参数时的场景
  • 客户端需要记得在使用前要先注册类型,有些累赘

不使用反射的类注册方式

如果我们不能使用反射,该怎么使用类注册方式呢?我们可以把注册的逻辑移到具体的产品类中去。这样一来,工厂类不再需要知道具体的产品类,添加新的产品类型也不需要修改产品类。

首先,我们在抽象的产品接口 Car 中增加一个生产实例的方法。

1
2
3
4
5
6
7
public interface Car {

  ...

  Car create();

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class BmwCar implements Car {

  static {
    CarFactoryWithoutReflection.instance()
        .registerCar(CarType.BMW, new BmwCar());
  }

  ...

  @Override
  public Car create() {
    return new BmwCar();
  }
}

上面我们注册了 Car 的类实例到工厂的注册表中。另外,我们在具体的产品类中实现了 create() 方法,用来在需要时生产各自的具体产品。

工厂类和它的使用方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
  public static class CarFactoryWithoutReflection {

    private Map<CarType, Car> registeredCars = new ConcurrentHashMap<>();

    private static CarFactoryWithoutReflection INSTANCE;

    private CarFactoryWithoutReflection() {
    }

    public static CarFactoryWithoutReflection instance() {
      if (INSTANCE == null) {
        INSTANCE = new CarFactoryWithoutReflection();
      }
      return INSTANCE;
    }

    public void registerCar(CarType type, Car car) {
      this.registeredCars.put(type, car);
    }

    public Car createCar(CarType type) {
      if (type == null) {
        throw new IllegalArgumentException("type cannot be null");
      }
      Car car = registeredCars.get(type);
      if (car == null) {
        throw new RuntimeException("Car type not registered: " + type);
      }
      return car.create();
    }

    // make sure car type is registered to the factory before creating cars
    static {
      try {
        Class.forName("me.donggan.patterns.factory.simple.model.BmwCar");
        Class.forName("me.donggan.patterns.factory.simple.model.VolvoCar");
        Class.forName("me.donggan.patterns.factory.simple.model.TeslaCar");
      } catch (ClassNotFoundException cnfe) {
        cnfe.printStackTrace();
      }
    }

    public static void main(String[] args) {

      Car car1 = CarFactoryWithoutReflection.instance().createCar(CarType.BMW);
      car1.drive();

      Car car2 = CarFactoryWithoutReflection.instance().createCar(CarType.VOLVO);
      car2.drive();

      Car car3 = CarFactoryWithoutReflection.instance().createCar(CarType.TESLA);
      car3.drive();
    }

  }

这种非反射的实现也有它的缺陷。

  • 为了确保类的注册逻辑被事先调用,客户端代码需要显式调用 Class.forName(),因此也比较累赘
  • 具体的产品类中耦合了类工厂,需要调用它的注册逻辑。

好在实际中,注册的部分通常都会由应用编程框架解决,因此以上的缺点可以减轻一部分。

工厂方法模式

如果我们再仔细回顾一下上面介绍的非反射的类注册方式,它由 Car 接口提供了一个 create() 方法。这个抽象接口用来提供对具体的生产对象逻辑的封装,使具体的创造逻辑被封装在具体的产品类中。这种行为更像是工厂应该做的事情。

另外,这个例子在静态代码中注册了 Car 的实例,后者仅仅用来调用多态的 create() 方法来制造其他的 Car 实例,这种设计多少有些尴尬。

如果我们对于每个具体的产品类型都提供一个相应的工厂,这个工厂仅仅用来生产相应的产品。例如,CarFactory 工厂的具体子类型有 BmwCarFactory, TeslaCarFactory and VolvoCarFactory,它们分别用来创建各自相应的 Car。这样,上面的尴尬就能避免了。实际上,这种调整就形成了工厂方法模式Factory Method Pattern)。

本文的续篇中,我将会详细介绍一下工厂方法模式