原型模式(克隆生成对象)
前言
暂时抛弃掉之前的上下文(机器人 Samu
与主人 Alice
),创建型模式总不能很好对应机器人的上下文。
因为原型模式足够简单,所以才让人误解原型模式并不那么值得深入了解(因为起初我也是众多轻视者中的其中之一),但是事实上它可以很重要。
正文
__ 继承__ 对于熟知面向对象开发的我们是再熟悉不过了,我们通常使用一个类继承另外一个类来实现对类中属性与方法的共享。 那么我们能否使用一个对象继承另外一个对象,来实现对象之间的属性、方法的共享呢?
程序员视角
思考: 如何建立对象级的继承关系?
继承的目的是为了共享父类的属性与方法,此时需要抛开脑海中的基于类才可以实现继承
的刻板思想。
这样子对象
需要父对象
的方法和属性的时候,可以通过聚合父对象
来访问父对象
的方法、属性。
但是直接直接聚合父对象
会引起问题:父对象的变更,会对子对象产生影响。(这在Java类继承中是不可接受的)。
于是我们想到了原型模式
,即克隆父对象
并让子对象
持有父对象(克隆)
的引用,借此来避免这个问题。
代码实现
声明原型链接口,用于实现如上图的聚合关系。
public interface IPrototype {
// 原型对象(即它的父类对象)
IPrototype __proto__();
// 自身构造器
IPrototype __constructor__();
// 定义ProtoTypeInherits方法的目的是为了演示,通过 原型链的"对象继承"
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface ProtoTypeInherits {
}
}
定义原型对象基类(用于模拟对象继承对象的类)
public class ProtoObject implements IPrototype, Cloneable {
// 克隆的原型对象
private ProtoObject cloneProto;
public ProtoObject(/* 继承的对象 */ProtoObject cloneProto) {
if (cloneProto == null) {
this.cloneProto = null;
} else {
try {
this.cloneProto = cloneProto.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
@Override
public IPrototype __proto__() {
return cloneProto;
}
@Override
public IPrototype __constructor__() {
return this;
}
@Override
protected ProtoObject clone() throws CloneNotSupportedException {
return (ProtoObject) super.clone();
}
// 省略了部分与调用功能相关的代码
}
分类定义三个对象,分别是Root(根节点)、Combination(枝干)、Leaf(叶子)
对象。
public class Root extends ProtoObject {
public Root(ProtoObject cloneProto) {
super(cloneProto);
}
@ProtoTypeInherits
public void root() {
}
}
public class Combination extends ProtoObject {
public Combination(ProtoObject cloneProto) {
super(cloneProto);
}
@ProtoTypeInherits
public void combination() {
}
}
public class Leaf extends ProtoObject {
public Leaf(ProtoObject cloneProto) {
super(cloneProto);
}
@ProtoTypeInherits
public void leaf() {
}
}
注意:这3个对象并没有类继承关系。
public class Client {
public static void main(String[] args) {
// 是否找到了 装饰者模式 的影子?
ProtoObject root = new Root(null);
ProtoObject combination = new Combination(root);
ProtoObject leaf = new Leaf(combination);
leaf.printChain();
}
}
到现在为止,我们已经成功的构建了基于对象聚合的原型链
。构造原型的代码如下:
public class ProtoObject implements IPrototype, Cloneable {
public ProtoObject(/* 继承的对象 */ProtoObject cloneProto) {
try {
this.cloneProto = cloneProto.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
@Override
protected ProtoObject clone() throws CloneNotSupportedException {
return (ProtoObject) super.clone();
}
}
ProtoObject
实现了Cloneable
,原型其实就是这么简单。当然在Java中提及Cloneable
一定会提到深拷贝
、浅拷贝
。
上下文
:你有房子A,后来你买了房子B。现在呢,你想把房子A打造的和房子B一模一样,怎么办呢?(我要克隆A对象)
深拷贝
:所以你给房子B,买了房子A中所有的家具。(2套家具)。
浅拷贝
:所以你把房子A中的家具全部搬到了房子B。(1套家具)。
当然理解这些的前提是,知道Java引用与C指针的区别。
所以对于对象继承我们不可避免的要使用深拷贝
,所以使用对象继承的一个显著的缺点:会产生大量的碎片对象(父对象)
。回头再来想想Java的类继承机制:类的对象只会在内存中存留一份,继承关系中需要使用到父类的方法是通用引用的方式访问父类。(不要太好 :-P )
工作还没有做完,现在已经有了原型链
。接下需要实现通过原型链
访问父对象的方法
。
public class ProtoObject implements IPrototype, Cloneable {
public void invoke(String methodString) {
IPrototype proto = __constructor__();
Class<?> cls;
System.out.printf(" 查找 原型链 + (%s)%n", methodString);
while (proto != null) {
cls = proto.getClass();
if (pickInvoke(cls, proto, methodString, (Method method) -> (method != null))) {
break;
}
proto = proto.__proto__();
}
System.out.printf(" 查找 原型链 - (%s)%n", methodString);
}
private boolean pickInvoke(Class<?> cls, IPrototype proto, String methodString, Predicate<Method> predicate) {
Method method = pickMethod(cls, methodString::equalsIgnoreCase);
if (predicate.test(method)) {
System.out.println("当前输入类型" + cls.getSimpleName() + ",结果匹配成功 ");
try {
method.invoke(proto);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return true;
} else {
System.out.println("当前输入类型" + cls.getSimpleName() + ",结果匹配失败 ");
return false;
}
}
public Method pickMethod(Class<?> cls, Predicate<String> predicate) {
Method[] methods = cls.getMethods();
if (methods == null) return null;
if (methods.length == 0) return null;
for (Method m : methods) {
if (predicate.test(m.getName())) {
return m;
}
}
return null;
}
// 省略无关的代码
}
这里使用到了JDK 1.8
中的Lambda
语法,如果你还不了解JDK 1.8
可以持续关注我的另一篇文集JDK 1.8之旅。
public class Client {
public static void main(String[] args) {
ProtoObject root = new Root(null);
ProtoObject combination = new Combination(root);
ProtoObject leaf = new Leaf(combination);
// 通过反射调用父类的方法
leaf.invoke("root");
}
}
// 输出日志
// 查找 原型链 + (root)
// 当前输入类型Leaf,结果匹配失败
// 当前输入类型Combination,结果匹配失败
// 当前输入类型Root,结果匹配成功
// 查找 原型链 - (root)
通过对象继承对象
的功能我们已经初步实现了,原型模式在其中也发挥了不少的力气 — — 不关心对象的具体创建过程、初始化过程,但同时也因为原型模式产生了大量的内存对象。
总结
原型模式的本质:克隆生成对象。
使用原型对象可以封装对象的创建过程,至于对象的初始化过程是否需要封装则依据使用者的需求决定。
原型对象会产生大量的内存对象,所以请勿过度使用原型模式。如果的确需要大量使用原型模式,请考虑结合原型管理器缓存原型实例。