查看原文
其他

Bruce Eckel:再聊设计模式(篇三)工厂模式

Bruce Eckel 中生代技术
2024-08-23

Bruce Eckel

读完需要

10分钟

速读仅需 1 分钟


布鲁斯 • 埃克尔(Bruce Eckel),C++ 标准委员会的创始成员之一,知名技术顾问,专注于编程语言和软件系统设计方面的研究,常活跃于世界各大顶级技术研讨会。
他自 1986 年以来,累计出版 Thinking in C++、Thinking in Java、On Java 等十余部经典计算机著作,曾多次荣获 Jolt 最佳图书奖(被誉为“软件业界的奥斯卡”),其代表作 Thinking in Java 被译为中文、日文、俄文、意大利文、波兰文、韩文等十几种语言,在世界范围内产生了广泛影响。

Bruce Eckel:再聊设计模式(篇一)设计模式分类

Bruce Eckel:再聊设计模式(篇二)封装实现


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岁...




继续滑动看下一个
中生代技术
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存