模式简介
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在软件系统中,有时候需要多次创建某一类型的对象,为了简化创建过程,可以只创建一个对象,然后再通过克隆的方式复制出多个相同的对象,这就是原型模式的设计思想。
原型模式的基本工作原理是通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象复制原型自己来实现创建过程。
举例说明 👇
《西游记》中孙悟空拔毛变小猴的故事几乎人人皆知,孙悟空可以用猴毛根据自己的形象,复制出很多跟自己长得一模一样的身外身来。
孙悟空这种复制出多个身外身的方式在面向对象设计领域里称为原型(Prototype)模式。在面向对象系统中,使用原型模式来复制一个对象自身,从而克隆出多个与原型对象一模一样的对象。
在软件系统中,有些对象的创建过程较为复杂,而且有时候需要频繁创建。原型模式通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象,这就是原型模式的意图所在。
模式结构
原型模式包含如下角色 👇
- Prototype(抽象原型类)
抽象原型类是定义具有克隆自己的方法的接口,是所有具体原型类的公共父类,可以是抽象类,也可以是接口。
- ConcretePrototype(具体原型类)
具体原型类实现具体的克隆方法,在克隆方法中返回自己的一个克隆对象。
- Client(客户类)
客户类让一个原型克隆自身,从而创建一个新的对象。在客户类中只需要直接实例化或通过工厂方法等方式创建一个对象,再通过调用该对象的克隆方法复制得到多个相同的对象。
模式案例
原型模式的克隆分为浅克隆和深克隆。
- 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
下面通过两个分别实现浅克隆和深克隆的实例来进一步学习并理解原型模式。
案例1:原型模式实例之邮件复制(浅克隆)
- 抽象原型类Object(无需创建)
Object作为抽象原型类,在Java语言中,所有的类都是Object的子类,在Object 中提供了克隆方法clone(),用于创建一个原型对象,其 clone()方法具体实现由JVM完成,用户在使用时无须关心。
- 附件类 Attachment
为了更好地说明浅克隆和深克隆的区别,在本实例中引入了附件类Attachment,邮件类Email与附件类是组合关联关系,在邮件类中定义一个附件类对象,作为其成员对象。
package prototype; /** * @author mengzhichao * @create 2021-11-10-22:20 */ public class Attachment { public void download(){ System.out.println("下载附件"); } }
- 具体原型类Email(邮件类)
Email类是具体原型类,也是Object类的子类。在Java语言中,只有实现了Cloneable接口的类才能够使用clone()方法来进行复制,因此Email类实现了Cloneable接口。在Email类中覆盖了Object的clone()方法,通过直接或者间接调用Object的clone()方法返回一个克隆的原型对象。在Email类中定义了一个成员对象attachment,其类型为Attachment。
package prototype; /** * @author mengzhichao * @create 2021-11-10-22:18 */ public class Email implements Cloneable { private Attachment attachment=null; public Email(){ this.attachment=new Attachment(); } @Override public Object clone(){ Email clone =null; try { clone=(Email) super.clone(); }catch (CloneNotSupportedException e){ System.out.println("Clone failure!"); } return clone; } public Attachment getAttachment(){ return this.attachment; } public void display(){ System.out.println("查看邮件"); } }
- 客户端测试类Client
在Client 客户端测试类中,比较原型对象和复制对象是否一致﹐并比较其成员对象attachment的引用是否一致。
package prototype; /** * @author mengzhichao * @create 2021-11-10-22:29 */ public class Client { public static void main(String[] args) { Email email,copyEmail; email=new Email(); copyEmail= (Email) email.clone(); System.out.println("email == copyEmail?"); System.out.println(email == copyEmail); System.out.println("email.getAttachment() == copyEmail.getAttachment()?"); System.out.println(email.getAttachment() == copyEmail.getAttachment()); } }
结果如下
通过结果可以看出,表达式(email==copyEmail)结果为false,即通过复制得到的对象与原型对象的引用不一致,也就是说明在内存中存在两个完全不同的对象,一个是原型对象,一个是克隆生成的对象。
但是表达式(email.getAttachment( ) == copyEmail.getAttachment())结果为true,两个对象的成员对象是同一个,说明虽然对象本身复制了一份,但其成员对象在内存中没有复制,原型对象和克隆对象维持了对相同的成员对象的引用。
案例2:原型模式实例之邮件复制(深克隆)
使用深克隆实现邮件复制,即复制邮件的同时复制附件。
- 附件类 Attachment
作为Email类的成员对象,在深克隆中,Attachment类型的对象也将被写入流中,因此Attachment类也需要实现Serializable接口。
package prototype2; import java.io.Serializable; /** * @author mengzhichao * @create 2021-11-10-22:20 */ public class Attachment implements Serializable { public void download(){ System.out.println("下载附件"); } }
- 具体原型类Email(邮件类)
Email作为具体原型类,由于实现的是深克隆,无须使用Object的 clone()方法,因此无须实现Cloneable接口;可以通过序列化的方式实现深克隆(代码中粗体部分),由于要将Email类型的对象写入流中,因此Email类需要实现Serializable接口。
package prototype2; import java.io.*; /** * @author mengzhichao * @create 2021-11-10-22:18 */ public class Email implements Serializable { private Attachment attachment=null; public Email(){ this.attachment=new Attachment(); } public Object deepClone() throws IOException,ClassNotFoundException, OptionalDataException{ //将对象写入流中 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()); } public Attachment getAttachment(){ return this.attachment; } public void display(){ System.out.println("查看邮件"); } }
- 客户端测试类Client
在Client客户端测试类中,我们仍然比较深克隆后原型对象和拷贝对象是否一致,并比较其成员对象attachment的引用是否一致。
package prototype2; /** * @author mengzhichao * @create 2021-11-11-22:28 */ public class Client { public static void main(String[] args) { Email email,copyEmail = null; email=new Email(); try { copyEmail = (Email) email.deepClone(); }catch (Exception e){ e.printStackTrace(); } System.out.println("email==copyEmail?"); System.out.println(email==copyEmail); System.out.println("email.getAttachment()==copyEmail.getAttachment()?"); System.out.println(email.getAttachment()==copyEmail.getAttachment()); } }
通过结果可以看出,表达式( email==copyEmail)结果为false,即通过复制得到的对象与原型对象的引用不一致,表达式( email.getAttachment()==copyEmail.getAttachment())结果也为false,原型对象与克隆对象对成员对象的引用不相同,说明其成员对象也复制了一份。
模式总结
优缺点:原型模式最大的优点在于可以快速创建很多相同或相似的对象,简化对象的创建过程,还可以保存对象的一些中间状态。
其缺点在于需要为每一个类配备一个克隆方法,因此对已有类进行改造比较麻烦﹐需要修改其源代码,并且在实现深克隆时需要编写较为复杂的代码。
适用于:创建新对象成本较大,新的对象可以通过原型模式对已有对象进行复制来获得。系统要保存对象的状态,而对象的状态变化很小,需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便
拓展内容
带原型管理器的原型模式
- 原型模式的一种改进形式是带原型管理器的原型模式。
原型管理器(Prototype Manager)角色创建具体原型类的对象,并记录每一个被创建的对象。原型管理器的作用与工厂相似,其中定义了一个集合用于存储原型对象,如果需要某个对象的一个克隆,可以通过复制集合中对应的原型对象来获得。在原型管理器中针对抽象原型类进行编程,以便扩展。
下面使用代码模拟演示一个颜色原型管理器的实现过程。
1.抽象原型类 MyColor
package prototypemanager; /** * @author mengzhichao * @create 2021-11-14-10:30 */ public interface MyColor extends Cloneable { public Object clone(); public void display(); }
- 具体原型类 Red
package prototypemanager; /** * @author mengzhichao * @create 2021-11-14-10:32 */ public class Red implements MyColor { @Override public Object clone() { Red r=null; try { r = (Red) super.clone(); }catch (Exception e){ } return r; } @Override public void display() { System.out.println("This is Red"); } }
- 具体原型类 Blue
package prototypemanager; /** * @author mengzhichao * @create 2021-11-14-10:36 */ public class Blue implements MyColor { @Override public Object clone() { Blue b=null; try { b = (Blue) super.clone(); }catch (Exception e){ } return b; } @Override public void display() { System.out.println("This is Blue"); } }
- 原型管理器类 PrototypeManager
package prototypemanager; import java.util.Hashtable; /** * @author mengzhichao * @create 2021-11-14-10:37 */ public class PrototypeManager { private Hashtable ht =new Hashtable(); public PrototypeManager() { ht.put("red",new Red()); ht.put("blue",new Blue()); } public void addColor(String key,MyColor obj){ ht.put(key,obj); } public MyColor getColor(String key){ return (MyColor) ((MyColor)ht.get(key)).clone(); } }
- 客户端测试类 Client
package prototypemanager; /** * @author mengzhichao * @create 2021-11-14-10:47 */ public class Client { public static void main(String[] args) { PrototypeManager pm =new PrototypeManager(); MyColor obj1=pm.getColor("red"); obj1.display(); MyColor obj2=pm.getColor("red"); obj2.display(); System.out.println(obj1==obj2); } }
运行结果如下
- 在PrototypeManager中定义了一个 Hashtable类型的集合,使用“键值对”来存储原型对象,客户端可以通过Key来获取对应原型对象的克隆对象。PrototypeManager类提供了工厂方法,用于返回一个克隆对象。