- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
原型模式(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):
从类图可以清晰看到两条关键关系:
Beast、Mage、Warlord均实现(..|>)Prototype,继承copy()克隆能力;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)
根据文档与源码,在以下场景应当考虑原型模式:
- 待实例化的类在运行时才确定,例如通过动态加载(
Class.forName等机制)获得具体类型; - 希望避免构建一套与产品类层次平行的工厂类层次——原型模式用一个持有原型的工厂即可覆盖整个产品族;
- 类的实例只可能有少数几种状态组合——预先安装好对应数量的原型,需要时克隆即可,比每次手工
new并逐字段设置状态更便捷; - 对象创建成本远高于克隆成本——例如对象构造涉及重量级初始化(数据库连接、IO 资源、复杂计算)时,克隆现成实例可避免重复初始化;
- 具体类在运行时仍然未知(与第 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
Design patterns implemented in Java
相关推荐
Java Builder 模式实战指南:基于 java-design-patterns 仓库的 Hero 构建器深度解析
Java Builder 模式实战指南:基于 java design patterns 仓库的 Hero 构建器深度解析 Builder(构建器)模式是 Gan
示例工程教程Java 回调模式(Callback Pattern)详解:基于 java-design-patterns 的异步通信实现指南
Java 回调模式(Callback Pattern)详解:基于 java design patterns 的异步通信实现指南 本文以 java design
示例工程教程Druid SQL 解析器重构实录:统一 DialectFeature 门控模式与 tableAlias() 方法分解
Druid SQL 解析器重构实录:统一 DialectFeature 门控模式与 tableAlias 方法分解 导读 本文以 Druid(阿里云 DataW
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考