Java设计模式-原型模式

原型(Prototype)模式,在23种设计模式中属于对象创建型模式。

原型模式就是用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。

引言

初次接触原型模式,如果仅根据模式名称判断,可能并不确定这种设计模式究竟是做什么的。不过,因为由于以前有了解过Java服务端开发,所以有印象S2SH框架有使用过prototype关键字,估计现在很少用S2SH框架了,基本都是Spring全家桶了。

如下是Spring采用XML方式配置JavaBean的方式。

< bean  id = "userBean"  class = "com.sunny.demo.User"  scope = "prototype" />

这里配置JavaBean使用的scope是prototype方式,其实还有另外一种singleton方式,singleton是默认的配置方式,如果配置scope的值为prototype,则在运行过程中会生成多个User类型的实例,如果是singleton方式,则生成的User类型实例仅有一个。

其实原型模式就是相对于单例模式而言的另外一种设计模式,单例模式是在整个运行周期内仅存在一个单实例,而原型模式却可以存在多个实例。原型模式允许一个对象再创建另外一个可定制的对象,无须知道任何创建的细节,同时又能保证性能。

对于很多熟悉Java语言的开发者来说,可能会将原型模式直接与克隆联系在一起,认为原型模式就是使用Java的clone方式创建新对象。但是我们知道Java创建新对象的方式不止一种,使用new或者反射技术都可以,使用克隆的好处是可以将已有对象的属性和方法直接复制给新的对象。克隆技术确实是原型模式中最常使用的方式之一,其实有时候如果对象的结构比较简单,采用new方式创建对象反而性能更高。另外需要记住一点,Java中clone都是浅克隆,想了解更多关于克隆内容可以参看Java浅谈克隆clone

适用性

原型模式多用于创建复杂或者耗时的实例,因为这种情况下,复制一个已经存在的实例使程序运行更高效;或者值相等,只是命名不一样的同类数据。

  • 创建新对象成本较大,比如构造方法初始化过程中需要消耗非常多的资源,这些资源包括数据或者硬件等资源。 一个对象多个访问者场景。一个对象需要提供给其它对象访问,而且访问者可能需要修改其值时,可以考虑原型模式复制多个对象供访问者调用。
  • 为了避免创建一个与产品类层次平行的工厂类层次时。

Java clone原型示例

public class PrototypeBean implements Cloneable {
	public String strAttr;
	public int intAttr;

	public PrototypeBean(String strAttr, int intAttr) {
		this.strAttr = strAttr;
		this.intAttr = intAttr;
	}

	@Override
	public String toString() {
		return "PrototypeBean [strAttr=" + strAttr + ", intAttr=" + intAttr + "]";
	}

	@Override
	public PrototypeBean clone() {
		PrototypeBean bean = null;
		try {
			bean = (PrototypeBean) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return bean;
	}
}
public class Client {
	public static void main(String[] args) {	
		PrototypeBean bean=new PrototypeBean("hello",1001);
		
		PrototypeBean clone=bean.clone();
		System.out.println(bean);
		System.out.println(clone);
		System.out.println("-------------------");
		clone.strAttr="hello world";
		System.out.println(bean);
		System.out.println(clone);
		
	}
}

自定义原型示例

// 抽象原型类
public interface Prototype {
	// 定义一个类似clone的方法
	Prototype copy();
}

// 具体原型类
public class PrototypeBean implements Prototype {

	// 省略属性以及构造方法
	
	// 将当前对象的属性赋值给新创建的对象
	@Override
	public PrototypeBean copy() {
		return new PrototypeBean(strAttr, intAttr);
	}
}

原型管理器示例

// 抽象原型类
public abstract class Document implements Cloneable {
	protected String type;

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

	abstract void display();

	@Override
	public Document clone() {
		try {
			return (Document) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return null;
	}
}

// 具体原型类
public class TxtDocment extends Document {
	public TxtDocment() {
		super("TXT");
	}

	@Override
	void display() {
		System.out.println("文件类型:" + type);
	}

	@Override
	public TxtDocment clone() {
		return (TxtDocment) super.clone();
	}
}

// 原型管理器
public class PrototypeManager {
	public static final int DOC_TXT = 1001;
	public static final int DOC_HTML = 1002;
	private Map map = new HashMap<>();

	private PrototypeManager() {
		map.put(DOC_TXT, new TxtDocment());
		map.put(DOC_HTML, new HtmlDocment());
	}

	public void addDocument(int type, Document doc) {
		map.put(type, doc);
	}

	public  T getDocument(int type) {
		return (T) map.get(type).clone();
	}

	public static PrototypeManager getInstance() {
		return InstanceHolder.INSTANCE;
	}

	private static class InstanceHolder {
		private static PrototypeManager INSTANCE = new PrototypeManager();
	}
}

public class Client {
	public static void main(String[] args) {
		PrototypeManager mgr=PrototypeManager.getInstance();
		
		HtmlDocment html01=mgr.getDocument(PrototypeManager.DOC_HTML);
		HtmlDocment html02=mgr.getDocument(PrototypeManager.DOC_HTML);
		System.out.println(html01==html02);// false
		html01.display();// 文件类型:HTML
		html02.display();// 文件类型:HTML
		
		TxtDocment txt=mgr.getDocument(PrototypeManager.DOC_TXT);
		txt.display();// 文件类型:TXT
	}
}

模式分析

原型模式类图有两种形式,一种标准原型模式类图,一种是带有原型管理器类图。

  • Prototype抽象原型类:它声明克隆方法的接口,是所有具体原型类的父类,可以是抽象类、接口或者具体类。
  • ConcretePrototype具体原型类:它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
  • PrototypeManager原型管理器:将多个原型对象存储在一个集合中供客户端使用, 它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。
  • Client客户端:让一个原型克隆自身返回一个新的对象。

原型模式中声明一个带有clone方法的接口并实现该接口,这里的接口可以是Java中原生的clone接口,也可以是自定义的可以返回原型类型实例的接口。在实现该接口中方法时,有Java这种原生就支持clone的实现固然好,当然了,如果不支持原生clone方式,可以自己实现一个类似克隆的操作,比如new一个对象,然后将原型对象的属性再一一复制给新创建的对象。

如果使用的是Java语言实现原型模式,那么相对于new一个新对象来说,clone一个对象少了一步调用构造方法的操作,这一点尤其需要注意。

模式优点

创建复杂对象时,原型可以简化创建过程,通过复制已有实例可以提高创建效率。

可以使用深度克隆保存对象状态,使用原型模式将对象复制一份并将其状态保存起来,可辅助实现撤销操作,恢复历史状态。

模式缺点

需要为每一个类提供一个克隆方法,而且这个克隆方法位于类的内部,当对已有类进行改造时,必须修改该类源代码,违背了“开闭原则”。

在实现深克隆特别是包含循环引用时的复杂对象需要编写较为复杂的代码。

与其它模式关系

原型模式和工厂模式在某种方面是相互竞争的。但是它们也可以一起使用。Abstract Factory可以存储一个被克隆的原型的集合,并且返回产品对象,这里的Factory有点类似于原型管理器Prototype Manager功能。

有时候原型可以作为备忘录模式的一个简化版本,其条件是你需要在历史记录中存储的对象的状态比较简单,不需要链接其他外部资源,或者链接可以方便地重建。

结束语

在Java语言中,一般实现原型模式都是基于clone方式实现,但是由于Java中clone都是浅克隆,所以在使用过程中需要特别注意,尤其是对象中仍然包含其它对象的引用。

public class User {
	public String name;

	public User(String name) {
		this.name = name;
	}
}
public class ListBean implements Cloneable {
	public ArrayList list = new ArrayList<>();

	public ListBean() {
		list.add(new User("hello"));
	}

	@Override
	public ListBean clone() {
		ListBean bean = null;
		try {
			bean = (ListBean) super.clone();
			bean.list = (ArrayList) list.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return bean;
	}
}

从上述示例可以知道ListBean不仅克隆了自己,还对自己的成员变量list进行了克隆,但是这里仍可能会引发其它问题,可以看一下如下示例的输出结果。

ListBean bean = new ListBean();
ListBean clone = bean.clone();
System.out.println(bean.list.get(0).name);// hello
System.out.println(clone.list.get(0).name);// hello
System.out.println("----------");

clone.list.get(0).name = "你好";
System.out.println(bean.list.get(0).name);// 你好
System.out.println(clone.list.get(0).name);// 你好
System.out.println("----------");

clone.list.remove(0);
System.out.println(bean.list.size());// 1
System.out.println(clone.list.size());// 0

在原型对象和克隆对象中成员变量list确实是两个不同的实例变量,但是list变量中持有的User类型的引用仍然是同一个对象,我们对克隆对象中的User对象name的更改也影响了原型对象中name的值。

深克隆有一种简单的实现方式,就是采用序列化和反序列化方式,但是这种方式也有限制,需要所有的对象类都必须实现序列化接口,而且需要注意对象中带有transient修饰的成员变量。

public static  T clone(T t) throws IOException, ClassNotFoundException{
	// 序列化
	ByteArrayOutputStream bos = new ByteArrayOutputStream();
	ObjectOutputStream oos = new ObjectOutputStream(bos);
	oos.writeObject(t);
	// 反序列化
	ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
	ObjectInputStream ois = new ObjectInputStream(bis);
	
	return (T) ois.readObject();
}

原型模式就是以一个对象为原型,然后创建出其它新对象的方式,一般多用于创建复杂耗时的对象。原型模式在实现上一般采用克隆的方式,克隆的好处是可以将已有对象的属性和方法直接复制给新的对象。在使用克隆时也有许多需要注意的地方,克隆不会调用类的构造方法,也不会克隆final类型的变量,而且在实现深克隆是需要克隆所有引用类型的变量。

评论

您确定要删除吗?删除之后不可恢复