原型模式(Prototype Pattern)Java 实战指南:基于 java-design-patterns 的克隆创建深入解析

原创2026-10-01 09:54:101,675 阅读
文章标签:示例工程后端

原型模式(Prototype Pattern)Java 实战指南:基于 java-design-patterns 的克隆创建深入解析

原型模式(Prototype Pattern)是一种创建型设计模式,其核心思想是用一个原型实例来指定待创建对象的类型,并通过克隆(Clone) 而不是 new 来产生新对象。在 java-design-patterns 仓库中,prototype 模块用一套完整的"兽人/精灵军团"示例演示了这一模式从抽象基类、克隆工厂到测试验证的完整落地方式。读完本文,你将掌握 Object.clone() 的原生克隆机制、如何用泛型抽象 copy() 方法、如何通过原型工厂批量生成对象,以及浅克隆与深克隆的取舍。

模式意图与适用场景

原型模式的意图可以用一句话概括:通过一个原型实例指定要创建的对象类型,并通过复制这个原型来创建新对象。它属于 GoF 提出的创建型设计模式之一,在 prototype/README.md 中被标注为 Creational 分类,并带有 Gang Of Four 与 Instantiation 两个标签。

Wikipedia 的定义与本仓库文档一致:

原型模式是一种创建型设计模式,当需要创建的对象类型由一个原型实例决定时,就克隆该原型以产生新对象。

用更朴素的话说:基于一个已存在的对象,通过克隆来创建新对象。它允许你复制一个已有对象,再按需修改它,而不是从零开始创建并手工配置一个对象。

需要注意的是,原型模式并非为了性能优化而设计——文档中特别强调,它不是用来获取性能收益的,而仅仅用于从原型实例创建新对象。

现实世界的类比

文档给出了一个广为流传的例子:克隆羊多莉(Dolly)。多莉是克隆技术的产物,原型模式的要点与之完全一致——一切都是关于"克隆"。其他典型类比还包括:

  • 定制家具工厂:工厂保留最受欢迎设计的原型,客户下单时直接克隆原型并做必要的定制,而不是从零开始打造每件家具,从而在保证质量一致性的同时快速交付;
  • 游戏开发:批量创建属性相似的小兵、敌人对象(如 prototype/README.md 所述);
  • GUI 库:用原型创建按钮、窗口等控件。

程序化示例:用克隆生产精灵与兽人

本仓库的 prototype 模块位于 prototype 目录,包结构为 com.iluwatar.prototype。整个示例包含三套类层次:Mage(法师)、Warlord( warlord 首领)与 Beast(野兽),每套又分别有精灵(Elf)与兽人(Orc)两种实现。

第一步:抽象克隆接口

在 Java 中实现原型模式,推荐的方式是创建一个带克隆方法的抽象基类。本示例中的 Prototype<T> 基类通过泛型化的 copy() 方法完成克隆(源码见 Prototype.java):

public abstract class Prototype<T> implements Cloneable {

  @SuppressWarnings("unchecked")
  @SneakyThrows
  public T copy() {
    return (T) super.clone();
  }
}

这里有几个关键技术点值得展开:

  • implements Cloneable:Object.clone() 是受保护的原生方法,只有实现了 Cloneable 标记接口的类才能调用它,否则会抛出 CloneNotSupportedException;
  • @SneakyThrows(Lombok):super.clone() 声明抛出受检异常 CloneNotSupportedException,用 @SneakyThrows 可以"偷偷"绕过受检异常,让 copy() 的调用方无需捕获,代码更简洁;
  • 泛型返回 (T) super.clone():利用泛型 T 让 copy() 返回具体子类型,配合 @SuppressWarnings("unchecked") 消除强制转换警告,调用方无需再自行强转;
  • 默认浅克隆:Object.clone() 默认执行浅拷贝(shallow copy),即基本类型字段直接复制、引用类型字段复制引用本身。这一点对理解后续行为至关重要。

第二步:构建生物类层次

以 Beast(野兽)与 OrcBeast(兽人野兽)为例(源码见 Beast.java 与 OrcBeast.java):

@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
public abstract class Beast extends Prototype<Beast> {

  public Beast(Beast source) {
  }

}
@EqualsAndHashCode(callSuper = false)
@RequiredArgsConstructor
public class OrcBeast extends Beast {

  private final String weapon;

  public OrcBeast(OrcBeast orcBeast) {
    super(orcBeast);
    this.weapon = orcBeast.weapon;
  }

  @Override
  public String toString() {
    return "Orcish wolf attacks with " + weapon;
  }
}

值得注意的设计细节:

  • 抽象基类 Beast 提供了一个拷贝构造函数 Beast(Beast source),虽然当前为空实现,但它为子类提供了"基于已有对象构造新对象"的语义入口——这是原型模式中常见的深克隆备用路径;
  • OrcBeast 通过 @RequiredArgsConstructor(Lombok)生成接受 weapon 的构造器,同时手写了 OrcBeast(OrcBeast orcBeast) 拷贝构造器,逐字段复制 weapon;
  • @EqualsAndHashCode(callSuper = false) 确保克隆后的对象与原型值相等(equals 语义一致),这一点在测试中被直接验证;
  • 每个具体类重写 toString(),形成可读的输出文本。

同样的结构还存在于 Mage(Mage.java)、Warlord(Warlord.java)及其精灵/兽人实现中:

抽象基类 精灵实现 兽人实现 状态字段
Beast ElfBeast("Elven eagle helps in ...") OrcBeast("Orcish wolf attacks with ...") helpType / weapon
Mage ElfMage OrcMage helpType / weapon
Warlord ElfWarlord OrcWarlord helpType / weapon

完整的类结构可以从类图源文件 prototype.urm.puml 中确认:Beast、Mage、Warlord 三个抽象类分别实现 Prototype,精灵/兽人具体类继承各自抽象基类,HeroFactoryImpl 同时依赖并持有这三种原型对象。

第三步:用原型工厂批量生产对象

为了充分发挥原型模式的价值,仓库提供了 HeroFactory 接口与其实现 HeroFactoryImpl(源码见 HeroFactory.java 与 HeroFactoryImpl.java):

public interface HeroFactory {

  Mage createMage();
  Warlord createWarlord();
  Beast createBeast();
}
@RequiredArgsConstructor
public class HeroFactoryImpl implements HeroFactory {

  private final Mage mage;
  private final Warlord warlord;
  private final Beast beast;

  public Mage createMage() {
    return mage.copy();
  }

  public Warlord createWarlord() {
    return warlord.copy();
  }

  public Beast createBeast() {
    return beast.copy();
  }
}

工厂的三个字段(mage、warlord、beast)就是原型对象,通过 @RequiredArgsConstructor 由构造器注入。每次调用 createXxx() 都执行 prototype.copy(),即克隆原型并返回一个独立的新实例。这正是原型模式相对抽象工厂模式的差异所在:

  • 抽象工厂模式通过工厂方法创建新对象;
  • 原型模式通过克隆原型实例产生新对象——客户端完全不需要知道具体类名,也不知道对象是如何被构造的。

第四步:完整运行示例

入口程序 App.java 展示了完整流程——先构造一组精灵原型,再替换为一组兽人原型,然后分别克隆出法师、首领和野兽:

public static void main(String[] args) {
    var factory = new HeroFactoryImpl(
            new ElfMage("cooking"),
            new ElfWarlord("cleaning"),
            new ElfBeast("protecting")
    );
    var mage = factory.createMage();
    var warlord = factory.createWarlord();
    var beast = factory.createBeast();
    LOGGER.info(mage.toString());
    LOGGER.info(warlord.toString());
    LOGGER.info(beast.toString());

    factory = new HeroFactoryImpl(
            new OrcMage("axe"),
            new OrcWarlord("sword"),
            new OrcBeast("laser")
    );
    mage = factory.createMage();
    warlord = factory.createWarlord();
    beast = factory.createBeast();
    LOGGER.info(mage.toString());
    LOGGER.info(warlord.toString());
    LOGGER.info(beast.toString());
}

运行该示例的控制台输出如下:

Elven mage helps in cooking
Elven warlord helps in cleaning
Elven eagle helps in protecting
Orcish mage attacks with axe
Orcish warlord attacks with sword
Orcish wolf attacks with laser

可以看到:换一套原型对象,工厂立即能产出完全不同种族和能力的生物——客户端的创建逻辑完全没有变化,这正是原型模式"面向原型编程"的威力。

类图与序列图

下图展示了 prototype 模块的完整类结构,包括 Prototype 抽象克隆基类、三个生物抽象类、六个具体实现类以及 HeroFactory/HeroFactoryImpl 工厂层次(图片来源 prototype/etc/prototype.urm.png):

原型模式类图:Prototype 基类、Beast/Mage/Warlord 抽象层、精灵与兽人实现及 HeroFactory 工厂

从类图可以清晰看到两条关键关系:

  1. Beast、Mage、Warlord 均实现(..|>)Prototype,继承 copy() 克隆能力;
  2. HeroFactoryImpl 组合(-->)持有 mage、warlord、beast 三个原型字段,并实现 HeroFactory 接口。

时序上,工厂调用链为:客户端调用 HeroFactoryImpl.createMage() → 委托 mage.copy() → 内部执行 super.clone() 返回新实例 → 返回给客户端。完整时序可参考 prototype/etc/prototype-sequence-diagram.png。

测试验证:克隆的正确性由 JUnit 保障

仓库为原型模式提供了参数化单元测试 PrototypeTest.java,用 6 组(3 兽人 + 3 精灵)原型对象验证克隆的四个关键性质:

@ParameterizedTest
@MethodSource("dataProvider")
void testPrototype(P testedPrototype, String expectedToString) {
    assertEquals(expectedToString, testedPrototype.toString());

    final var clone = testedPrototype.copy();
    assertNotNull(clone);                      // 克隆结果非空
    assertNotSame(clone, testedPrototype);     // 是全新对象(不同引用)
    assertSame(testedPrototype.getClass(), clone.getClass()); // 类型一致
    assertEquals(clone, testedPrototype);      // 值相等(equals)
}

这个测试精确地定义了原型克隆的语义边界:

  • assertNotSame:克隆对象与原型不是同一个引用(不同对象);
  • assertSame(类):克隆对象与原型属于同一具体类(运行时类型不变);
  • assertEquals:克隆对象与原型值相等——这正是基类上用 @EqualsAndHashCode(callSuper = false) 的意义所在,Lombok 生成的 equals 保证按字段比较两个对象"内容相同"。

此外 AppTest.java 负责验证 main 入口可正常执行。

何时使用原型模式(Applicability)

根据文档与源码,在以下场景应当考虑原型模式:

  1. 待实例化的类在运行时才确定,例如通过动态加载(Class.forName 等机制)获得具体类型;
  2. 希望避免构建一套与产品类层次平行的工厂类层次——原型模式用一个持有原型的工厂即可覆盖整个产品族;
  3. 类的实例只可能有少数几种状态组合——预先安装好对应数量的原型,需要时克隆即可,比每次手工 new 并逐字段设置状态更便捷;
  4. 对象创建成本远高于克隆成本——例如对象构造涉及重量级初始化(数据库连接、IO 资源、复杂计算)时,克隆现成实例可避免重复初始化;
  5. 具体类在运行时仍然未知(与第 1 点互为补充)。

已知应用与相关知识

原型模式最经典的 Java 原生体现就是 java.lang.Object#clone() 方法。本仓库的 Prototype.copy() 本质上就是对该方法的封装与泛型化。围绕克隆机制,还有几个需要明确的实践知识:

  • 浅克隆 vs 深克隆:Object.clone() 默认浅克隆,若对象图中含有可变引用字段,浅克隆会让克隆体与原型共享同一引用,可能引发副作用;需要深克隆时,通常借助拷贝构造函数或序列化等方式逐层复制。这也是原型模式实现中最容易出错、最复杂的部分;
  • Cloneable 标记接口:不实现它而调用 clone() 会抛出 CloneNotSupportedException;
  • 规避受检异常:仓库用 Lombok 的 @SneakyThrows 优雅处理,若不用 Lombok 则需自行 try/catch 或将异常抛出。

模式收益与代价

收益:

  • 隐藏了实例化新对象的复杂过程,客户端只面向 copy()/工厂方法;
  • 减少系统中类的数量——无需为每种产品配备专属工厂类;
  • 支持在运行时动态地增加和移除对象类型。

代价:

  • 必须实现克隆机制,复杂对象图的克隆实现难度较高;
  • 深克隆难以正确实现,尤其当类拥有包含循环引用的复杂对象图时。

与其他设计模式的关系

  • 抽象工厂模式:两者都涉及对象创建,但抽象工厂通过工厂方法创建对象,原型模式则通过克隆原型实例创建对象,二者可以互补(原型可作为抽象工厂的实现手段);
  • 单例模式:若允许克隆单例实例,单例可以利用原型来产生实例;
  • 组合模式:原型常被用于组合结构中,以便动态创建组件树。

如何运行示例

prototype 模块是 Maven 多模块项目 java-design-patterns 下的独立子模块(见 prototype/pom.xml),其 maven-assembly-plugin 已配置 com.iluwatar.prototype.App 作为主类。在仓库根目录运行:

./mvnw -pl prototype compile exec:java

或使用 Maven Assembly 打包后运行:

./mvnw -pl prototype package
java -jar prototype/target/prototype-*.jar

运行后将看到上文展示的 6 行日志输出。整个示例仅依赖 slf4j-api、logback-classic(日志)与 junit-jupiter(测试),无其他框架负担,非常适合作为理解原型模式的入门范本。

登录后查看全文
java-design-patterns