掘金 后端 ( ) • 2024-04-08 10:20

一、原型模式介绍

1. 解决的问题

主要解决的问题就是创建重复对象,这部分对象内容本身比较复杂,生成过程可能从库中或者RPC接口中获取数据的耗时较长,因此采用克隆的方式节省时间。

2. 定义

原型模式是一种创建型模式,能够复制已有对象,而又无需使代码依赖它们所属的类。

3. 应用场景

  • 对象的初始化需要很多其他对象的数据准备或其他资源的繁琐计算。

  • 需要复制一些对象,同时又希望代码独立于这些对象所属的具体类。

    这种场景通常出现在代码需要处理第三方代码通过接口传递过来的对象时。即使不考虑代码耦合的情况,我们的代码也不能依赖这些对象所属的具体类,因为不知道其具体信息。

    原型模式为我们的代码提供了一个通用接口,我们可以通过这一接口与所有实现了克隆的对象进行交互,使得我们的代码与所克隆的对象具体类独立开。

  • 子类的区别仅在于其对象的初始化方式,可以使用原型模式来减少子类的梳理。

    创建这些子类的目的可能只是为了创建特定类型的对象。

原型模式已与 Java 融为一体,可以随手拿来使用:

  • Objectclone() 方法。
  • Cloneable 接口的实现类,至少可以看到一千多个实现。

4. 浅拷贝与深拷贝

在 Java 中,数据类型分为值类型和引用类型,值类型包括 int、double、byte、boolean、char 等基本数据类型,引用类型包括类、接口、数组等复杂类型。

浅拷贝与深拷贝的主要区别在于是否支持引用类型的成员变量的复制

  1. 浅拷贝

    在浅拷贝中,若原型对象的成员变量是值类型,将复制给克隆对象;若原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,即原型对象和克隆对象的成员变量指向相同的内存地址。

    简单来说,在浅拷贝中,对象被复制时只复制其本身和其包含的值类型的成员变量,而引用类型的成员对象并没有复制

    在 Java 中,通过覆盖 Object 类的 clone() 方法即可实现浅拷贝。

  2. 深拷贝

    在深拷贝中,无论原型变量的成员变量是值类型还是引用类型,都将复制给克隆对象。

    简单来说,在深拷贝中,除了对象本身被复制外,对象所包含的所有成员变量也将复制

    在 Java 中,实现深克隆,可以通过复制引用对象或序列化读取二进制流等方式实现,需要注意的是,能够序列化的对象必须实现 Serializable 接口

    实现 Serializable 读取二进制流示例:

    protected Object deepClone() throws CloneNotSupportedException, IOException, ClassNotFoundException {
        //将对象写入流中
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(this);
    ​
        //将对象从流中取出
        ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (ois.readObject());
    }
    

二、原型模式优缺点

1. 优点

  • 可以克隆对象,而无需与其所属的具体类相耦合。
  • 可以克隆生成原型,避免反复运行初始化代码。
  • 可以更方便的生成复杂对象。
  • 可以使用继承以外的方式来处理复杂对象的不同配置。

2. 缺点

  • 克隆包含循环引用的复杂对象可能会非常麻烦。

三、原型模式应用实例:英雄联盟齐天大圣-真假猴王

1. 实例场景

英雄联盟中有一个英雄叫齐天大圣 · 孙悟空,这个名字想必我们都很熟悉,而在英雄联盟中,它的人物背景是这样的:

悟空是一个瓦斯塔亚族的机灵鬼,用自己的力量、灵敏和机智迷惑对手并抢得先机。机缘巧合让他结识了一位剑客并与之成为一生的挚友,这位剑客被人称作易大师。后来,悟空就成为了古老武术门派“无极”的最后一位弟子。如今,附魔长棍傍身的悟空,目标是让艾欧尼亚免遭崩溃的命运。

“我的游记才刚刚开始……”——齐天大圣 · 孙悟空

齐天大圣 · 孙悟空有一个技能叫做真假猴王:

孙悟空进入持续1秒的隐形状态并朝着一个方向突进300距离(不能越过墙体),同时留下一个会攻击附近敌人的分身。

孙悟空的分身会模仿孙悟空的普通攻击和终极技能,但是仅造成35/40/45/50/55%伤害。

今天我们就使用创造孙悟空的分身作为模拟场景。

2. 原型模式实现

2.1 工程结构
builder-pattern
└─ src
    ├─ main
    │   └─ java
    │    └─ org.design.pattern.prototype
    │       └─ model
    │          ├─ Hero.java
    │          └─ WuKong.java
    └─ test
        └─ java
            └─ org.design.pattern.prototype.test
                └─ WuKongTest.java
2.2 代码实现

英雄基类

/**
 * 英雄基类
 */
@Getter
@Setter
@NoArgsConstructor
public abstract class Hero {
    /**
     * 生命值
     */
    protected int healthPoint;
​
    /**
     * 法力值
     */
    protected int magicPoint;
​
    /**
     * 物理伤害
     */
    protected int attackDamage;
​
    /**
     * 法术伤害
     */
    protected int abilityPower;
​
    public Hero(Hero hero) {
        if (hero != null) {
            this.healthPoint = hero.healthPoint;
            this.magicPoint = hero.magicPoint;
            this.attackDamage = hero.attackDamage;
            this.abilityPower = hero.abilityPower;
        }
    }
​
    public abstract Hero clone();
​
    /**
     * 攻击
     */
    public void attach() {
        System.out.printf("This hero’s attack caused %d damage", this.attackDamage);
        System.out.println();
    }
}

齐天大圣 · 孙悟空

/**
 * 齐天大圣孙悟空
 */
@Getter
@Setter
@NoArgsConstructor
public class WuKong extends Hero {
    /**
     * 物理伤害比例
     */
    protected float attackDamageProportion = 1;
​
    public WuKong(WuKong wuKong) {
        super(wuKong);
        if (wuKong != null) {
            this.healthPoint = wuKong.healthPoint;
            this.magicPoint = wuKong.magicPoint;
            this.attackDamage =  (int) (wuKong.attackDamage * wuKong.attackDamageProportion);
            this.abilityPower = wuKong.abilityPower;
        }
    }
​
    @Override
    public WuKong clone() {
        return new WuKong(this);
    }
​
    /**
     * 攻击
     */
    public void attach() {
        super.attach();
    }
}
2.3 测试验证
2.3.1 测试验证类
public class WuKongTest {
    @Test
    public void testWuKong() {
        //齐天大圣孙悟空
        WuKong wuKong = new WuKong();
        wuKong.setHealthPoint(540);
        wuKong.setMagicPoint(300);
        wuKong.setAttackDamage(68);
        wuKong.setAbilityPower(54);
        //复制孙悟空分身
        wuKong.setAttackDamageProportion(0.3f);
        WuKong fakeKong = wuKong.clone();
        System.out.println("孙悟空攻击:");
        wuKong.attach();
        System.out.println("孙悟空分身攻击:");
        fakeKong.attach();
    }
}
2.3.2 测试结果
孙悟空攻击:
This hero’s attack caused 68 damage
孙悟空分身攻击:
This hero’s attack caused 20 damage

四、原型模型结构

原型模式-模式结构图.png

  1. 原型 (Prototype) 接口将对克隆方法进行声明。在绝大多数情况下,其中只会有一个名为 clone克隆的方法。
  2. 具体原型 (Concrete Prototype) 类将实现克隆方法。 除了将原始对象的数据复制到克隆体中之外, 该方法有时还需处理克隆过程中的极端情况,例如克隆关联对象和梳理递归依赖等等。
  3. 客户端 (Client) 可以复制实现了原型接口的任何对象。

设计模式并不难学,其本身就是多年经验提炼出的开发指导思想,关键在于多加练习,带着使用设计模式的思想去优化代码,就能构建出更合理的代码。

源码地址:https://github.com/yiyufxst/design-pattern-java

参考资料:

重学 Java 设计模式:https://juejin.cn/post/6850037264849928206

深入设计模式:https://refactoringguru.cn/design-patterns/creational-patterns