Design-Pattern-Prototype
原型模式(Prototype)
是创建型模式中最简单、最不重要的一个设计模式。说不重要不是因为用不到,而是在实践过程中通常使用了其他更简便的方法进行了替代。前两篇设计模式在撰写时文本量超过了预期,本篇尽量言简意赅。
场景
假设现在有个对象,需要根据这个对象中的属性,去创建新的同类对象。这么做的目的可能是重新创建对象过程复杂,直接从已有对象中复制相当于节省了许多步骤,可以快速获取大量同类对象。也可能是需要保留一份对象当前的状态。这样的需求十分常见。
在没有学过设计模式或一些现代开发技巧前,想当然的解决办法是,先通过new
关键字新建一个对象,然后反复调用其set
方法,设置值来自调用原型中的get
方法。一些语言,例如C++,也可以通过拷贝构造函数,来完成对象的克隆。
1 |
|
这样做有时行得通,但需要满足一些前提条件。例如这需要被克隆的类需要提供所有字段的get
和set
的公共方法。例如客户端需要明确被克隆类的具体类型,才能使用new
关键字创建具体类对象。客户端也需要知道关于被克隆类的细节,有哪些字段,增加了代码的耦合度。
原型模式提供了一个方案,基于一个已存在的对象克隆生成新的对象,而无需知道具体实现细节。
原型模式
在不知道被克隆对象类型的情况下,需要根据运行时具体类型产生不同运行结果,通常会利用多态特性,并在具体类中定义克隆方法,在克隆方法中新建自身类的对象,并完成字段从原型到新对象的复制。由于方法定义在类中,因此也不涉及字段的访问权限问题,也做到了克隆过程和客户端分离的作用。
在Java
中,所有类都继承于Object
类。在Object
类中,声明了一个克隆方法,方法的签名如下:
1 |
|
protected
意味着所有继承的子类均可调用这个方法,native
关键字则声明方法是由本地语言实现的,而不是由
Java
语言实现的。这个方法在JDK
中的注释如下:
The method clone for class Object performs a specific cloning operation. First, if the class of this object does not implement the interface Cloneable, then a CloneNotSupportedException is thrown. Note that all arrays are considered to implement the interface Cloneable and that the return type of the clone method of an array type T[] is T[] where T is any reference or primitive type. Otherwise, this method creates a new instance of the class of this object and initializes all its fields with exactly the contents of the corresponding fields of this object, as if by assignment; the contents of the fields are not themselves cloned. Thus, this method performs a "shallow copy" of this object, not a "deep copy" operation. The class Object does not itself implement the interface Cloneable, so calling the clone method on an object whose class is Object will result in throwing an exception at run time.
在每个对象上调用这个方法时,会先检查具体类是否实现了Cloneable
接口,然后新建对象,并完成了所有字段的浅拷贝。这个Object
类的clone
方法,相当于帮我们完成了new
对象和字段复制。
如果想通过这种方式完成对象克隆,需要在具体类中实现Cloneable
接口。并重写clone
方法,将访问修饰符改为public
。方法体内可以简单调用super.clone()
,也就是Object
类的clone
方法。
1 |
|
上例代码中,对返回对象做了类型转换。原方法返回Object
类型,重写时可以返回其子类类型。这是因为在此具体类中,clone
方法返回对象的类型是确定的,可以在此直接完成类型转换,而不是由客户端执行类型转换。
在java
之外的语言中,由于无法调用Object
类的clone
方法,也没有Cloneable
接口,可以在方法体中通过调用空参或有参构造函数,再进行字段设置并返回来实现。以下是C++
中的实现。
1 |
|
总之,最简单的原型模式就是在类中定义一个拷贝方法,在拷贝方法中完成创建自身类对象,属性复制,并返回对象的操作。
抽象产品拓展
在上一章中,只涉及了一种产品类的复制。产品类的拓展模式在前些篇中已出现多次,被细分为抽象产品和具体产品。如果在需要客户端中使用抽象产品作为变量类型,调用克隆方法,则这意味着克隆方法需要声明在接口中。在java
中,可以通过继承Cloneable
接口,并声明返回接口类型的克隆方法。并在子类中实现这个克隆方法。
1 |
|
抽象产品也可以定义为抽象类。如果所有子类都是简单调用Object
中的clone
方法,则可以在抽象类中实现Cloneable
接口,并提供方法默认实现。
1 |
|
如果语言未提供类似java
的Cloneable
接口,则可以自定义一个接口,在接口中声明一个克隆方法,在抽象产品接口类中继承此接口,并在子类中实现克隆方法的具体实现。
现代方法
由于将属性从一个对象复制到另一个对象,这个功能是如此的使用频繁,以至于许多工具类都提供类似功能。因此上文提到的原型模式,在实际使用中通常都被工具类的使用给替代了,使用频率并不高。
就java
来说,最常用的就是Apache BeanUtils
和Spring BeanUtils
。通常认为后者在可用性和性能上更好,使用更多。这两者都是通过反射特性来完成对象间属性拷贝。Spring BeanUtils
提供了静态方法,可以传入两个对象,即可把其中一个对象的属性复制到另一个当中。甚至传入的两个对象在类型上不需要有相同或继承的关系,只要有同名同类型的属性即可拷贝。hutool BeanUtil
还提供了静态方法,可以传入返回对象的类型,直接获取拷贝后的对象。
1 |
|
浅拷贝和深拷贝
在不同语言中,数据类型大致可以分为基础数据类型和引用数据类型。成员变量中,基础数据类型会将实际数值储存在对象中,而引用数据类型存储的是对象的地址,通过这个地址可以访问对象的属性和方法。
在复制对象时,对于基础数据类型,是内存区域的复制,实际数值会在两个对象中各保存一份;而对于引用数据类型,如果只是引用的复制,复制完两个引用仍指向了同一个对象。这种对于复制模式,称为浅拷贝。与之对应的深拷贝,则是将每个引用数据类型的对象,在内存中新new
了一份,并将返回的引用赋值给成员变量。
浅拷贝可能存在的问题是,复制完后,对其中一个对象进行了改动,另一个对象的内容也会随之改动,引用数据类型的成员变量不满足拷贝后的对象与原对象相互独立的特性。但由于深拷贝需要创建新的对象,会更耗费内存和时间。实际情况下,如果对象在复制完后,不会对其中引用数据类型进行变动,使用浅拷贝提升性能是可取的。
另外,对于一些不可变类型来说,由于每次变动都会新建一个对象,就算是引用数据类型,使用浅拷贝也是安全的。最典型的便是String
、包装数据类型,如果对象中只包含基础数据类型、String
和包装数据类型,那么使用浅拷贝也是安全的。
总结
最简单的原型模式,就是在产品类中定义一个拷贝方法,在方法体中完成新建对象和属性的复制。
如果产品类需要拓展,细分为抽象产品和具体产品,则需要在抽象产品中定义拷贝方法,返回类型设定为抽象产品类,并在具体产品类中实现拷贝方法。
现代开发习惯中,通常使用工具类来完成对象的拷贝,实现原型模式。
浅拷贝性能好,但可能存在拷贝后的对象与原型不独立的问题。需要根据实际情况来选择。如果成员变量都是不可变对象,则可以安全地使用浅拷贝。