Bruce Eckel:再聊设计模式(篇三)工厂模式
Bruce Eckel
读完需要
10分钟速读仅需 1 分钟
布鲁斯 • 埃克尔(Bruce Eckel),C++ 标准委员会的创始成员之一,知名技术顾问,专注于编程语言和软件系统设计方面的研究,常活跃于世界各大顶级技术研讨会。
他自 1986 年以来,累计出版 Thinking in C++、Thinking in Java、On Java 等十余部经典计算机著作,曾多次荣获 Jolt 最佳图书奖(被誉为“软件业界的奥斯卡”),其代表作 Thinking in Java 被译为中文、日文、俄文、意大利文、波兰文、韩文等十几种语言,在世界范围内产生了广泛影响。
6
工厂模式:封装对象的创建
如果一个已有系统必须接受新的类型,那么明智的第一步便是使用多态性为这些新类型创建一个公共接口。这样可以在系统中将代码和所增加的具体类型隔离开(不必知道具体是什么类型)。添加新的类型可以不影响已有代码——至少看起来是这样。
它会让你感觉,代码中唯一要做的改动就是在继承出新类型的地方进行调整,但事实并非如此。你仍然要创建新类型的对象,并且在创建的时候必须指定具体要使用的构造器。如果这样的类型创建操作遍布于程序中,增加新类型便意味着需要找出所有要注意类型的地方。
工厂会强制通过某个公共节点来创建对象,以防止用于创建的代码在系统中四处扩散。系统中的所有代码都必须通过该工厂创建某个对象。这样在向系统增加新类的时候,只需要修改工厂即可。
每个面向对象的程序都会创建对象。由于有时你编写的程序会通过增加新类型的方式进行扩展,因此你可能会发现工厂很有用。
接下来会通过一系列 Shape(形状)层次结构来演示工厂模式。如果某个 Shape 无法成功创建,则需要使用合适的异常:
// patterns/shapes/BadShapeCreation.java
package patterns.shapes;
public class BadShapeCreation
extends RuntimeException {
public BadShapeCreation(String msg) {
super(msg);
}
}
然后是 Shape 的基类:
// patterns/shapes/Shape.java
package patterns.shapes;
public class Shape {
private static int counter = 0;
private int id = counter++;
@Override public String toString() {
return
getClass().getSimpleName() + "[" + id + "]";
}
public void draw() {
System.out.println(this + " draw");
}
public void erase() {
System.out.println(this + " erase");
}
}
Shape 会自动为每个对象创建一个唯一的 id。toString()方法则通过反射检测出具体的 Shape 子类名称。
现在可以创建一些 Shape 类了:
// patterns/shapes/Circle.java
package patterns.shapes;
public class Circle extends Shape {}
// patterns/shapes/Square.java
package patterns.shapes;
public class Square extends Shape {}
// patterns/shapes/Triangle.java
package patterns.shapes;
public class Triangle extends Shape {}
工厂包含一个用于创建对象的方法:
// patterns/shapes/FactoryMethod.java
package patterns.shapes;
public interface FactoryMethod {
Shape create(String type);
}
create()的参数用于指定要创建的 Shape 的具体类型。本例中的参数正好是 String,但也可以是任何一组数据。这个初始化数据可能来自系统外部。
下面是一个可复用的,对 FactoryMethod 实现进行的测试:
// patterns/shapes/FactoryTest.java
package patterns.shapes;
import java.util.stream.*;
public class FactoryTest {
public static void test(FactoryMethod factory) {
Stream.of("Circle", "Square", "Triangle",
"Square", "Circle", "Circle", "Triangle")
.map(factory::create)
.peek(Shape::draw)
.peek(Shape::erase)
.count(); // 终结操作
}
}
记住,只有在最后加上终结操作,Stream 才会真的执行。此处丢弃了 count()的值。
还有一种实现工厂的方法,即显式地创建每种类型:
// patterns/ShapeFactory1.java
// 一个基本的静态工厂方法
import java.util.*;
import java.util.stream.*;
import patterns.shapes.*;
public class ShapeFactory1 implements FactoryMethod {
@Override public Shape create(String type) {
switch(type) {
case "Circle": return new Circle();
case "Square": return new Square();
case "Triangle": return new Triangle();
default:
throw new BadShapeCreation(type);
}
}
public static void main(String[] args) {
FactoryTest.test(new ShapeFactory1());
}
}
/* 输出:
Circle[0] draw
Circle[0] erase
Square[1] draw
Square[1] erase
Triangle[2] draw
Triangle[2] erase
Square[3] draw
Square[3] erase
Circle[4] draw
Circle[4] erase
Circle[5] draw
Circle[5] erase
Triangle[6] draw
Triangle[6] erase
*/
这样 create()便成了在添加一种新的 Shape 类型时,系统中唯一需要修改的另一处代码了。
6.1
动态工厂模式
由于 ShapeFactory1.java 封装了对象的创建过程,因此它是一种合理的解决方案。不过,如果可以在增加新类时完全不需要修改任何代码,甚至是工厂本身,则会更好。ShapeFactory2.java 使用了反射(在基础卷第 19 章中介绍过),在首次需要时,将某种具体 Shape 的 Constructor 动态加载到 factories map 中:
// patterns/ShapeFactory2.java
import java.util.*;
import java.lang.reflect.*;
import java.util.stream.*;
import patterns.shapes.*;
public class ShapeFactory2 implements FactoryMethod {
private Map<String, Constructor> factories =
new HashMap<>();
private static Constructor load(String id) {
System.out.println("loading " + id);
try {
return Class.forName("patterns.shapes." + id)
.getConstructor();
} catch(ClassNotFoundException |
NoSuchMethodException e) {
throw new BadShapeCreation(id);
}
}
@Override public Shape create(String id) {
try {
return (Shape)factories
.computeIfAbsent(id, ShapeFactory2::load)
.newInstance();
} catch(Exception e) {
throw new BadShapeCreation(id);
}
}
public static void main(String[] args) {
FactoryTest.test(new ShapeFactory2());
}
}
/* 输出:
loading Circle
Circle[0] draw
Circle[0] erase
loading Square
Square[1] draw
Square[1] erase
loading Triangle
Triangle[2] draw
Triangle[2] erase
Square[3] draw
Square[3] erase
Circle[4] draw
Circle[4] erase
Circle[5] draw
Circle[5] erase
Triangle[6] draw
Triangle[6] erase
*/
create()方法基于传入的 String 参数生成各种新的 Shape,它会将该参数作为键,在 factories HashMap 中进行查询,返回值为 Constructor,然后通过调用其上的 newInstance()创建新的 Shape 对象。
不过在刚开始运行该程序的时候,factories map 是空的。如果 Constructor 已存在于 Map 中,则 create()会通过 Map 的 computeIfAbsent()方法找到 Constructor。如果不在其中,则 computeIfAbsent()会通过 load()计算出 Constructor,并将其插入 Map。从输出可以看出,每个具体的 Shape 类型都只会在初次请求时进行加载,之后便可以直接从 Map 中获取了。
6.2
多态工厂模式
工厂方法模式可以从基类工厂中子类化出不同类型的工厂:
// patterns/ShapeFactory3.java
// 多态工厂方法
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import patterns.shapes.*;
interface PolymorphicFactory {
Shape create();
}
class RandomShapes implements Supplier<Shape> {
private final PolymorphicFactory[] factories;
private Random rand = new Random(42);
RandomShapes(PolymorphicFactory... factories) {
this.factories = factories;
}
@Override public Shape get() {
return
factories[rand.nextInt(factories.length)]
.create();
}
}
public class ShapeFactory3 {
public static void main(String[] args) {
RandomShapes rs = new RandomShapes( // [1]
Circle::new, Square::new, Triangle::new
);
Stream.generate(rs)
.limit(6)
.peek(Shape::draw)
.peek(Shape::erase)
.count();
}
}
/* 输出:
Triangle[0] draw
Triangle[0] erase
Circle[1] draw
Circle[1] erase
Circle[2] draw
Circle[2] erase
Triangle[3] draw
Triangle[3] erase
Circle[4] draw
Circle[4] erase
Square[5] draw
Square[5] erase
*/
由于 RandomShapes 是一个 Supplier
RandomShapes 的构造器接收 PolymorphicFactory 对象组成的可变参数列表。可变参数列表是以数组的形式传入的,因此这也是该列表在内部的存储形式。get()方法随机索引到该数组中,并对结果调用 create(),以生成新的 Shape。
RandomShapes 构造器调用([1])是唯一需要在增加新 Shape 类型时修改的地方。
虽然 ShapeFactory2.java 可能引发异常,但是在这个方法中没有引发——这在编译期是确定的。
6.3
抽象工厂模式
抽象工厂模式看起来很像我们之前见过的工厂对象,只不过它带有多个工厂方法,每个工厂方法都会生成一种不同的对象。工厂类型也有多种。选定了一种工厂对象,即确定了该工厂所创建出的所有对象的具体类型。《设计模式》中给出的示例在多种图形用户界面(GUI)间实现了可移植性。只需创建适用于所用 GUI 的工厂对象即可。如果向该工厂对象请求菜单、按钮、滑块等组件,它便会生成适合该 GUI 版本的组件。这样便隔离了在不同 GUI 间切换的影响。
假设你要创建一套通用的游戏环境,用来支持不同类型的游戏。用抽象工厂来实现,便可能是下面这样:
// patterns/abstractfactory/GameEnvironment.java
// 抽象工厂模式的示例
// {java patterns.abstractfactory.GameEnvironment}
package patterns.abstractfactory;
import java.util.function.*;
interface Obstacle {
void action();
}
interface Player {
void interactWith(Obstacle o);
}
class Kitty implements Player {
@Override
public void interactWith(Obstacle ob) {
System.out.print("Kitty has encountered a ");
ob.action();
}
}
class Fighter implements Player {
@Override
public void interactWith(Obstacle ob) {
System.out.print("Fighter now battles a ");
ob.action();
}
}
class Puzzle implements Obstacle {
@Override public void action() {
System.out.println("Puzzle");
}
}
class Weapon implements Obstacle {
@Override public void action() {
System.out.println("Weapon");
}
}
// 抽象工厂:
class GameElementFactory {
Supplier<Player> player;
Supplier<Obstacle> obstacle;
}
// 具体工厂:
class KittiesAndPuzzles
extends GameElementFactory {
KittiesAndPuzzles() {
player = Kitty::new;
obstacle = Puzzle::new;
}
}
class Melee
extends GameElementFactory {
Melee() {
player = Fighter::new;
obstacle = Weapon::new;
}
}
public class GameEnvironment {
private Player p;
private Obstacle ob;
public
GameEnvironment(GameElementFactory factory) {
p = factory.player.get();
ob = factory.obstacle.get();
}
public void play() {
p.interactWith(ob);
}
public static void main(String[] args) {
GameElementFactory
kp = new KittiesAndPuzzles(),
ml = new Melee();
GameEnvironment
g1 = new GameEnvironment(kp),
g2 = new GameEnvironment(ml);
g1.play();
g2.play();
}
}
/* 输出:
Kitty has encountered a Puzzle
Fighter now battles a Weapon
*/
Player 和 Obstacle 相互作用。每个游戏都有着不同类型的 Player 和 Obstacle。通过选择具体的 GameElementFactory 来确定具体的游戏。GameEnvironment 则会设置并运行游戏。在本例中,游戏的设置和运行都非常简单,但这些行为(初始条件和状态变化)可以很大程度上决定游戏的结果。这里的 GameEnvironment 并不是为继承而设计的,虽然继承也是一个可选项。
本例中也用到了双路分发(Double Dispatching)模式,稍后会讲解。(请关注下一章节:函数对象模式)
本书特色
查漏宝典:涵盖Java关键特性的设计原理和应用方法
避坑指南:以产业实践的得失为鉴,指明Java开发者不可不知的设计陷阱
经典普适:值得不同层次的Java开发者反复研读
专家领读:4位一线业务专家、知名作译者帮你拆解书中难点,总结Java开发精要
值得一提的是,为了帮助新手加深理解,出版方邀请了4位从业10年以上知名作译者(DDD 专家张逸、服务端专家梁桂钊、软件系统架构专家王前明、译者陈德伟)为本书录制【精讲视频】和【导读指南】,该视频已在B站和图灵社区发布,感兴趣的朋友可以去看看。
往期推荐
Bruce Eckel:再聊设计模式(篇一)
Bruce Eckel:再聊设计模式(篇二)封装实现
Bruce Eckel - 详解函数式编程(卷一)
Bruce Eckel - 详解函数式编程(卷二)
Bruce Eckel - 详解函数式编程(卷三)
聊聊 8 种架构模式
怎样才能持续输出技术原创文章?
同事多线程使用不当导致OOM,被我怒怼了
如何在 SpringBoot 项目中控制 RocketMQ消费线程数量
后Kubernetes时代的微服务
我,程序员,马上35岁...