# 原型模式

# 概念

原型模式( Prototype Pattern )是一种创建型设计模式,核心思想是通过复制现有对象(原型)来创建新对象,而无需通过 new 关键字和构造方法。该模式允许你创建对象的副本,同时保持代码与具体对象类的解耦。

# 作用

  1. 避免重复初始化开销:当对象创建过程复杂(如涉及数据库查询或复杂计算)时,复制已有对象比重新创建更高效。

  2. 简化对象创建过程:客户端无需知道对象创建细节,只需通过原型复制即可获得新对象。

  3. 动态配置对象类型:运行时通过改变原型对象来实例化新对象类型(如从对象池中选择原型)。

  4. 保护性拷贝:提供对象拷贝的安全机制,防止外部修改影响原始对象(深拷贝场景)。

# 场景

  1. 对象初始化成本高昂:需要避免重复执行耗时的初始化操作(如读取配置文件、数据库连接)。

  2. 对象状态变化频繁:需要基于当前状态快速创建相似对象(如游戏中的敌人复制)。

  3. 需要隔离对象副本:确保新对象与原型独立,修改互不影响(深拷贝场景)。

  4. 组合对象创建:复杂结构对象(如树形结构)的快速复制。

# 举例

假设我们有一个简历创建系统,用户可以创建自己的简历,并且希望能够快速地复制一份简历作为基础,然后进行修改。这里就可以使用原型模式:

package net.feixiang.creational.prototype;

/**
 * 工作经历类
 * 该类用于存储工作经历信息,包括公司名称和工作年限。它可以被 Resume 类使用。
 */
public class WorkExperience {
    private String company; // 公司名称
    private int workYears; // 工作年限

    // 构造方法
    public WorkExperience(String company, int workYears) {
        this.company = company;
        this.workYears = workYears;
    }

    // getter 和 setter 方法省略……

    @Override
    public String toString() {
        return "WorkExperience{" +
                "company='" + company + '\'' +
                ", workYears=" + workYears +
                '}';
    }
}
package net.feixiang.creational.prototype;

/**
 * 简历类
 * 包含姓名、性别、年龄和工作经历。实现了 Cloneable 接口以支持深拷贝。
 */
public class Resume implements Cloneable {
    private String name; // 姓名
    private String gender; // 性别
    private int age; // 年龄
    private WorkExperience workExperience; // 工作经历

    // 构造方法
    public Resume(String name, String gender, int age,
                     WorkExperience workExperience) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.workExperience = workExperience;
    }

    // 实现 clone 方法,深拷贝
    @Override
    protected Resume clone() {
        // 深拷贝:复制工作经历对象
        WorkExperience clonedWorkExperience = new WorkExperience(
                        this.workExperience.getCompany(),
                        this.workExperience.getWorkYears());
        return new Resume(this.name, this.gender, this.age, clonedWorkExperience);
    }

    // getter 和 setter 方法省略……

    @Override
    public String toString() {
        return "Resume{" +
                "name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                ", \nworkExperience=" + workExperience +
                '}';
    }
}

运行示例:

package net.feixiang.creational.prototype;

/**
 * 原型模式演示类
 * 该类演示了如何使用原型模式来复制简历对象。通过克隆原始简历对象,可以创建一个
 * 新的简历对象,并修改其属性。
 */
public class PrototypeDemo {
    public static void main(String[] args) {
        // 创建原始简历对象
        WorkExperience workExperience = new WorkExperience("飞翔软件公司", 3);
        Resume originalResume = new Resume("飞翔", "男", 28, workExperience);

        System.out.println("原始简历:" + originalResume);

        // 复制简历对象,这里是直接使用已存在的对象,而不是新建对象
        Resume clonedResume = originalResume.clone();
        clonedResume.setName("翱翔");
        clonedResume.getWorkExperience().setCompany("翱翔设计公司");

        System.out.println("复制简历:" + clonedResume);
    }
}

控制台输出:

原始简历:Resume{name='飞翔', gender='男', age=28, 
workExperience=WorkExperience{company='飞翔软件公司', workYears=3}}
复制简历:Resume{name='翱翔', gender='男', age=28, 
workExperience=WorkExperience{company='翱翔设计公司', workYears=3}}

# 反例

如果不使用原型模式,直接通过 new 关键字创建对象,可能会存在以下问题:

  1. 重复初始化问题

    如果对象的创建过程比较复杂,如需要进行大量的计算或数据访问等,那么每次创建新都需要对象重复这些初始化操作,导致系统性能下降。

  2. 无法灵活创建对象

    无法根据现有对象的状态快速创建新对象,需要重新指定所有属性的值,不够灵活。

  3. 代码可维护性差

    如果对象的结构发生变化,如增加新的属性或修改属性的类型等,那么所有创建该对象的地方都需要进行相应的修改,增加了代码的维护成本。

例如,在上述简历创建系统的场景中,如果不使用原型模式,每次创建新简历都需要重新输入姓名、性别、年龄以及工作经历等信息,无法快速地基于已有简历进行修改。而且,如果简历的结构发生变化,如增加教育背景等属性,那么所有创建简历的地方都需要修改,增加了代码的维护难度。

以下是不使用原型模式,而是通过构造方法直接创建副本的完整代码实现,该实现会导致对象共享问题:

package net.feixiang.creational.prototype.contrary;

import net.feixiang.creational.prototype.WorkExperience;

/**
 * 简历类
 * 该类演示了一个反例实现,使用构造方法创建副本,但只实现了浅拷贝,
 * 导致嵌套对象(工作经历)被共享。
 */
public class Resume {
    private String name; // 姓名
    private String gender; // 性别
    private int age; // 年龄
    private WorkExperience workExperience; // 工作经历

    // 构造方法
    public Resume(String name, String gender, int age,
                  WorkExperience workExperience) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.workExperience = workExperience;
    }

    /**
     * 通过构造方法创建副本(反例实现)
     * 问题:只实现了浅拷贝,嵌套对象会被共享
     */
    public Resume(Resume another) {
        this.name = another.name;
        this.gender = another.gender;
        this.age = another.age;
        // 错误:直接引用原始对象的工作经历
        this.workExperience = another.workExperience;
    }

    public void updateWorkExperience(String company, int years) {
        this.workExperience.setCompany(company);
        this.workExperience.setWorkYears(years);
    }

    // getter 和 setter 方法省略……

    @Override
    public String toString() {
        return "Resume{" +
                "name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                ", \nworkExperience=" + workExperience +
                '}';
    }
}

运行示例:

package net.feixiang.creational.prototype.contrary;

import net.feixiang.creational.prototype.WorkExperience;

/**
 * 浅拷贝演示类
 * 该类演示了一个反例实现,使用构造方法创建副本,但只实现了浅拷贝,
 * 导致嵌套对象(工作经历)被共享。
 */
public class ShallowCopyDemo {
    public static void main(String[] args) {
        // 创建原始简历
        WorkExperience originalExp = new WorkExperience("飞翔软件公司", 3);
        Resume originalResume = new Resume("飞翔", "男", 28, originalExp);

        // 通过构造方法创建副本(错误方式)
        Resume copiedResume = new Resume(originalResume);
        copiedResume.setName("翱翔");

        System.out.println("\n=== 修改前状态 ===");
        System.out.println("原始简历: " + originalResume);
        System.out.println("副本简历: " + copiedResume);

        // 修改副本的工作经历
        copiedResume.updateWorkExperience("翱翔设计公司", 5);

        System.out.println("\n=== 修改副本工作经历后 ===");
        System.out.println("原始简历: " + originalResume);  // 原始简历也被修改!
        System.out.println("副本简历: " + copiedResume);

        // 更危险的修改方式
        originalResume.getWorkExperience().setCompany("百度");

        System.out.println("\n=== 修改原始工作经历后 ===");
        System.out.println("原始简历: " + originalResume);
        System.out.println("副本简历: " + copiedResume);  // 副本简历也被修改!
    }
}

控制台输出:

=== 修改前状态 ===
原始简历: Resume{name='飞翔', gender='男', age=28, 
workExperience=WorkExperience{company='飞翔软件公司', workYears=3}}
副本简历: Resume{name='翱翔', gender='男', age=28, 
workExperience=WorkExperience{company='飞翔软件公司', workYears=3}}

=== 修改副本工作经历后 ===
原始简历: Resume{name='飞翔', gender='男', age=28, 
workExperience=WorkExperience{company='翱翔设计公司', workYears=5}}
副本简历: Resume{name='翱翔', gender='男', age=28, 
workExperience=WorkExperience{company='翱翔设计公司', workYears=5}}

=== 修改原始工作经历后 ===
原始简历: Resume{name='飞翔', gender='男', age=28, 
workExperience=WorkExperience{company='百度', workYears=5}}
副本简历: Resume{name='翱翔', gender='男', age=28, 
workExperience=WorkExperience{company='百度', workYears=5}}

# 解析


出现对象共享问题(浅拷贝陷阱):

  • 根本原因:构造方法中直接复制了 WorkExperience 对象的引用。

  • 风险:原始对象和副本对象共享同一个工作经历对象。

  • 后果:修改任意一个对象都会影响另一个对象,导致数据不一致。

# 原理

在Java中,使用原型模式时,通常需要满足以下条件:

  1. 实现 Cloneable 接口

    Cloneable 是一个标记接口,没有需要实现的方法。但它用于表明该类的对象可以被复制。如果一个类没有实现 Cloneable 接口,但却调用了 clone() 方法,会抛出 CloneNotSupportedException 异常。

  2. 重写 clone() 方法

    clone() 方法是 Object 类的一个 protected 方法。为了能够在外部分使用并定制复制逻辑,通常需要将其重写为 public 方法,并在其中实现具体的复制逻辑。

  3. 深拷贝与浅拷贝的考虑

    如果对象包含其他对象的引用(即组合对象),则需要考虑是进行浅拷贝还是深拷贝:

    • 浅拷贝: 只复制对象本身,不会复制其内部引用的对象,复制后的对象和原对象仍然引用相同的内部对象。

    • 深拷贝: 不仅复制对象本身,还会复制其内部引用的所有对象,确保复制后的对象与原对象完全独立。

在实际应用中,需要根据具体需求选择合适的拷贝方式。


总结


原型模式核心是通过复制现有对象创建新对象,无需 new 和构造方法,使代码与具体类解耦。作用包括避免重复初始化开销、简化创建过程、动态配置对象类型、提供保护性拷贝。实现时,类要实现 Cloneable 接口,重写 clone 方法,根据需求选择深拷贝或浅拷贝。

微信公众号

QQ交流群
原创网站开发,偏差难以避免。

如若发现错误,诚心感谢反馈。

愿你倾心相念,愿你学有所成。

愿你朝华相顾,愿你前程似锦。