把书读薄 | 《设计模式之美》设计模式与范式(创建型-原型模式)

0x0、引言

? 周末尝试戒游,抽空啃《设计模式之美》,本文对应设计模式与范式:创建型(47),原型模式 (Prototype Pattern) ,创建型设计设计模式的最后一种。了解此模式的应用场景,深浅拷贝的区别即可,同样非常简单。

二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。


0x1、定义与使用场景

又称 克隆模式,定义如下:

如果对象的 创建成本较大,且同一个类的不同对象间差别不大(大部分字段相同)的情况下,可以利用已有对象(原型)进行复制(或者叫拷贝、克隆)的方式来创建新的对象,以达到节省创建时间的目的。

简单点说就是以某个对象为原型,克隆出一个一模一样的对象,常见应用场景:

  • 资源优化 (对象初始化需使用较多外部资源时,如IO、数据库、CPU,权限等);
  • 复杂依赖 (依赖嵌套,A创建依赖B,B创建依赖C,一连串的对象get和set);
  • 同一对象多个修改者 (多个对象的调用者都需要修改对象的值,克隆多个供使用其使用,保护性拷贝);
  • 需保存原始对象状态 (如记录历史操作的场景,通过原型模式快速保存记录);
  • 结合工厂模式使用 (定义统一的复制接口,如clone、copy,使用一个工厂来统一进行拷贝和新对象创建,然后由工厂方法提供给调用者使用)。

组成角色


0x2、深浅拷贝

原型模式分为 浅拷贝深拷贝 两种,浅拷贝只复制对象中基本数据类型和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象…而深拷贝得到的则是一份完完整整独立的对象,所以,深拷贝更耗时和耗内存空间。

注:浅拷贝得到的对象和原始对象会共享部分数据,可能出现数据被修改的风险,故更适用于不可变对象。

很多开源框架和组件中都有原型模式的相关实现,并不一定非得从零去实现浅拷贝和深拷贝。下面以Java为例,讲解其中深浅拷贝相关的姿势~

① Java中==与equals的区别:

  • ==基本数据类型(int,long等) → 比较存储的值是否相等,引用型变量 → 比较所指向对象地址是否相等;
  • equals:不能用于比较基本数据类型,没有对equals()方法进行重写,默认比较指向的对象地址,若想比较对象内容,需自行重写此方法,做相应的判断。

② 克隆必须满足的三个条件

  • 对任何对象x,都有 x.clone() != x,即不是同一对象;
  • 对任何对象x,都有 x.clone().getClass == x.getClass(),即对象类型一致;
  • 如果对象obj的equals()方法定义得当的话,obj.clone().equals(obj)应该是成立的(推荐,不强制);

③ Java中浅拷贝的实现方式

想被克隆的类实现 Cloneable 接口,重写 clone() 方法,验证代码示例如下(省略get、set方法):

// 引用类型
public class Money {
    private String type;    // 币种

    public Money(String type) { this.type = type; }

    @Override
    public String toString() {
        return "Money{" + "type='" + type + '\'' + '}';
    }
}

// 原型类,实现Cloneable接口,重写clone方法
public class Assets implements Cloneable{
    private int amount;     // 数目
    private Money money;    // 币种
    private String kind;    // 资金种类

    public Assets(int amount, Money money, String kind) {
        System.out.println("执行了构造方法!");
        this.amount = amount;
        this.money = money;
        this.kind = kind;
    }

    @Override
    protected Object clone() {
        Assets assets = null;
        try {
            assets = (Assets)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return assets;
    }

    @Override
    public String toString() {
        return "Assets{" + "amount=" + amount + ", money=" + money + ", kind='" + kind + '\'' + '}';
    }
}

// 测试用例:
public class AssetsTest {
    public static void main(String[] args) {
        Assets a1 = new Assets(100, new Money("人民币"), "现金");
        Assets a2 = (Assets) a1.clone();
        System.out.println("a1.equals(a2) → " + a1.equals(a2));
        System.out.println("a1 == a2 → " + (a1 == a2));
        System.out.println("a1.getClass == a2.getClass → " + (a1.getClass() == a2.getClass()));
        System.out.println("a1.getMoney() == a2.getMoney() → " + (a1.getMoney() == a2.getMoney()));
        System.out.println("a1 → " + a1.toString());
        System.out.println("a2 → " + a2.toString());
        a1.setAmount(200);
        System.out.println("修改基本类型属性后:");
        System.out.println("a1 → " + a1.toString());
        System.out.println("a2 → " + a2.toString());
        a1.getMoney().setType("美金");
        System.out.println("修改引用用类型属性后:");
        System.out.println("a1 → " + a1.toString());
        System.out.println("a2 → " + a2.toString());
    }
}
复制代码

运行结果如下:

分析运行结果不难得出Java中浅拷贝具有如下特点:

  • 执行clone方法,不会调用构造方法;
  • 克隆会生成新的对象变量,但指向同一个内存地址;
  • 克隆前后数据类型一致;
  • 克隆时,基本数据类型属性会新建,引用类型只会生成一个新的引用变量,依旧指向同一个内存地址;

④ Java中深拷贝的两种实现方式

相比浅拷贝,深拷贝会连引用类型数据也新建,Java中实现深拷贝的两种方式:

  • 引用类型也实现Cloneable接口,重写clone()方法,原型类clone()方法调用引用类类型的clone()为money对象赋值:
@Override
protected Object clone() {
    Assets assets = null;
    try {
        assets = (Assets)super.clone();
        assets.setMoney((Money)this.getMoney().clone());
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    return assets;
}
复制代码

注:深拷贝需要整个克隆涉及的对象都正确的实现clone()方法,其只能怪一个没正确实现克隆,都会导致深拷贝失败。

  • 序列化,定义一个方法完成对象转二进制流和二进制流转对象,然后返回反序列化后的对象。
public Assets deepClone() {
    try {
        // 写入当前对象的二进制流
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        // 读取二进制流产生新对象
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (Assets)ois.readObject();
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
    return null;
}
复制代码

0x3、加餐:四类创建型设计模式总结

  • 单例模式 → 创建全局唯一的对象;

  • 工厂模式 → 创建不同但类型相关的对象(继承同一父类或接口),由给点参数决定构建那种类型的对象;

  • 建造者模式 → 创建复杂对象,通过设置不同的可选参数,定制化创建不同对象;

  • 原型模式 → 针对创建成本较大的对象,利用已有对象进行复制的方式创建,以此达到节省创建时间的目的;

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享