前言
学习之余也不要忘了生活,最近钻到书本里了,女朋友吃醋了(别问我女朋友哪来的,凭自己努力new的)。
原型模式是一种特殊的创建型模式,它通过复制一个已有的对象来获取更多相同或者相似的对象。原型模式可以提高相同类型对象的创建效率,简化创建过程。
正文
-
概述
使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。
原型模式的设计思想如下:
当客户端需要创建一个新的对象的时候,我们通过原型对象复制自身来创建出这个新的对象,需要注意的是,通过这种方式创建的对象是一个全新的对象,不过拥有与原型对象完全相同的属性,但它拥有新的地址值。原型模式是一种“另类”的创建型模式,创建新对象(就是克隆自身)的工厂就是原型自身,工厂方法由负责克隆对象的克隆方法来实现。 -
结构与实现
-
深克隆与浅克隆
浅克隆: 浅克隆中被复制(新建一份)的对象只包含他本身和值类型的成员变量,而其引用类型的成员变量不会被复制(新建一份)。这是什么意思呢?就是说当一个对象被浅克隆是,他属性中的引用数据类型不会被重新复制一份,即引用数据类型的属性指向的仍然是同一个地址值,当一个引用数据类型的属性被修改时,所有的复制该原型对象的对应属性都会被修改。我们可以通过手动新建对象赋值的方式来实现浅克隆,当然也可以使用Java的特性clone()方法来实现复制。
深克隆: 与浅克隆不同,深克隆会将对象所有的成员变量都复制新的一份,也就是复制完成之后的对象包括对象的成员变量都会指向新的地址,随意修改其中的值都不会影响其它的复制对象。
在java中,一般我们可以通过重写超类Object的clone方法以及Cloneable接口来实现浅克隆,通过序列化与反序列化的方式来实现深克隆,这两种方式在下面都会有学习。 -
模式结构
- Prototype(抽象建原型类): 声明克隆方法的接口,是所有具体原型类的公共父类(Java中可以不去定义,默认是Object),它可以是抽象类也可以是接口,甚至可以是具体的实现类。
- ConcretePrototype(具体原型类): 实现了抽象原型类中的克隆方法,返回一个自己的克隆对象。
- Client(客户类): 实例化一个原型对象,通过该对象的clone方法快速获取更多类似对象。
-
实现案例
我们直接通过具体的案例来了解原型模式。
案例说明:
在使用某个办公系统时,有些岗位的员工发现他们每周的工作都大同小异,每次填写周报时很多内容都是重复的,为了提高工作周报的创建效率,大家希望能有一种机制创建相同或者相似的周报,包括创建周报的附件,试用原型模式解决该问题。
案例分析:
需要有周报的内容大致相同,然后又要每个周报是不同的对象,这就需要一份周报作为原型,其余的周报都可以克隆(深克隆)此原型。
目录结构:
Attachment: 附件类。
public class Attachment { private String name; // 附件名 public String getName() { return name; } public void setName(String name) { this.name = name; } public void download(){ System.out.println("下载附件:"+name); } @Override public String toString() { return "Attachment{" + "name='" + name + '\'' + '}'; } }
WeeklyReport(具体产品类): 这里的抽象产品类就是Object,具体产品类实现了Object的clone方法,这是Java的特性,可以通过实现clone方法实现复制对象,但是需要注意的是实现该方法的类必须实现Cloneable接口,如果一个类的clone方法被调用但是没有实现Cloneable方法,那么编译器将会抛出一个CloneNotSupportedException异常。
public class WeeklyReport implements Cloneable{ private Attachment attachment; // 附件,一般可以有多个(用list来表示) private String name; private String date; private String content; public Attachment getAttachment() { return attachment; } public void setAttachment(Attachment attachment) { this.attachment = attachment; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDate() { return date; } public void setDate(String date) { this.date = date; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } // 使用clone方法实现浅克隆 @Override public WeeklyReport clone() throws CloneNotSupportedException { Object obj = null; try { obj = super.clone(); return (WeeklyReport) obj; } catch (CloneNotSupportedException e) { System.out.println("不支持复制!"); return null; } } @Override public String toString() { return "WeeklyReport{" + "attachment=" + attachment + ", name='" + name + '\'' + ", date='" + date + '\'' + ", content='" + content + '\'' + '}'; } }
Client(客户端测试类): 用以测试的客户端。
public class Client { /** * 需求: * 在使用某个办公系统时,有些岗位的员工发现他们每周的工作都大同小异,每次填写周报时很多内容都是重复的,为了提高工作周报的创建效率,大家 * 希望能有一种机制创建相同或者相似的周报,包括创建周报的附件,试用原型模式解决该问题. * 分析: * 需要有周报的内容大致相同,然后又要每个周报是不同的对象,这就需要一份周报作为原型,其余的周报都可以克隆(深克隆)此原型。 * @param args */ public static void main(String[] args) throws Exception { // 浅克隆实现,修改下clone出的周报的附件,原型周报的附件也会更改,但是我们要求附件等属性也是可以更改的,所以需要进行深克隆 simpleClone(); // 使用序列化从头实现深克隆 //darkClone(); // 测试下原型管理器 // testPrototypeManager(); } private static void testPrototypeManager() throws IOException, ClassNotFoundException { PrototypeManager manager = new PrototypeManager(); DarkWeeklReport darkWeeklReport = manager.get(); System.out.println(darkWeeklReport); } public static void darkClone() throws IOException, ClassNotFoundException { DarkWeeklReport weeklyReport_prototype,weeklyReport_new; weeklyReport_prototype = new DarkWeeklReport(); DarkAttachment attachment = new DarkAttachment(); attachment.setName("原型周报附件.txt"); weeklyReport_prototype.setAttachment(attachment); weeklyReport_prototype.setName("原型周报"); weeklyReport_prototype.setDate("2019-12-2"); weeklyReport_prototype.setContent("原型周报内容"); weeklyReport_new = weeklyReport_prototype.deepClone(); System.out.println("原型:"+weeklyReport_prototype); System.out.println("克隆:"+weeklyReport_new); // 修改下clone出的周报的附件,原型周报的附件也会更改 weeklyReport_new.getAttachment().setName("克隆周报附件.doc"); weeklyReport_prototype.getAttachment().download(); System.out.println("是否是同一个周报对象:"+(weeklyReport_new == weeklyReport_prototype)); System.out.println("是否是同一个附件对象:"+(weeklyReport_new.getAttachment() == weeklyReport_prototype.getAttachment())); } public static void simpleClone() throws CloneNotSupportedException { WeeklyReport weeklyReport_prototype,weeklyReport_new; weeklyReport_prototype = new WeeklyReport(); Attachment attachment = new Attachment(); attachment.setName("原型周报附件.txt"); weeklyReport_prototype.setAttachment(attachment); weeklyReport_prototype.setName("原型周报"); weeklyReport_prototype.setDate("2019-12-2"); weeklyReport_prototype.setContent("原型周报内容"); weeklyReport_new = weeklyReport_prototype.clone(); System.out.println("原型:"+weeklyReport_prototype); System.out.println("克隆:"+weeklyReport_new); // 修改下clone出的周报的附件,原型周报的附件也会更改 weeklyReport_new.getAttachment().setName("克隆周报附件.doc"); weeklyReport_prototype.getAttachment().download(); System.out.println("是否是同一个周报对象:"+(weeklyReport_new == weeklyReport_prototype)); System.out.println("是否是同一个附件对象:"+(weeklyReport_new.getAttachment() == weeklyReport_prototype.getAttachment())); }
运行结果:
可以发现,引用类型Attachment克隆对象与原型对象引用的是同一个Attachment对象,这时当我们修改克隆对象的Attachment时,原型对象的该属性也会被修改,因此我们需要用深克隆对该系统进行修改(强烈建议把这些代码自己跑跑看一下)。
-
-
深入探讨深克隆
深克隆方式: 为了解决上面提出的问题,我们现在不再使用Object的clone方法来进行克隆,而实自定义一个方式实现深克隆,该方法较常用的实现方式是使用序列化与反序列化的方式实现(首先使用需要劣化将对象写入到流中,然后使用反序列化将对写的从流中获取,由于在序列化时一个对象的成员会伴随着该对象写入到流中,反序列化时将得到一个包含成员对象的新对象,因此可以采用序列化与反序列化实现深克隆)。
为了方便大家的理解,我们将深克隆的实现类放到了另一个包中。
深克隆的附件类(实现了Serializable接口表示该类可以被序列化):public class DarkAttachment implements Serializable{ private String name; // 附件名 public String getName() { return name; } public void setName(String name) { this.name = name; } public void download(){ System.out.println("下载附件:"+name); } @Override public String toString() { return "Attachment{" + "name='" + name + '\'' + '}'; } }
深克隆周报类:clone方法改为了自定义的deepClone方法(通过序列化的方式实现复制)
public class DarkWeeklReport implements Serializable{ private DarkAttachment attachment; // 附件,一般可以有多个(用list来表示) private String name; private String date; private String content; public DarkAttachment getAttachment() { return attachment; } public void setAttachment(DarkAttachment attachment) { this.attachment = attachment; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDate() { return date; } public void setDate(String date) { this.date = date; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } // 使用序列化实现深克隆 // 首先使用需要劣化将对象写入到流中,然后使用反序列化将对写的从流中获取,由于在序列化时一个对象的成员会伴随着该对象写入到流中, // 反序列化时将得到一个包含成员对象的新对象,因此可以采用序列化与反序列化实现深克隆 public DarkWeeklReport deepClone() throws IOException, ClassNotFoundException { // 将对象写入到流中 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(outputStream); oos.writeObject(this); // 将对象从流中取出 ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); ObjectInputStream ois = new ObjectInputStream(inputStream); return (DarkWeeklReport) ois.readObject(); } @Override public String toString() { return "WeeklyReport{" + "attachment=" + attachment + ", name='" + name + '\'' + ", date='" + date + '\'' + ", content='" + content + '\'' + '}'; } }
Client客户端: 修改了客户端类的main方法
public static void main(String[] args) throws Exception { // 浅克隆实现,修改下clone出的周报的附件,原型周报的附件也会更改,但是我们要求附件等属性也是可以更改的,所以需要进行深克隆 //simpleClone(); // 使用序列化从头实现深克隆 darkClone(); // 测试下原型管理器 //testPrototypeManager(); }
运行结果:
这时我们就发现了,深克隆方式实现克隆出的对象中所有的成员变量都是全新的(不过是他们的值相等),每一个引用类型的属性的地址值也都是不同的。补充
原型管理器: 原型管理器将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,它的内部包含了一个集合用于存储原型对象,如果需要某个原型对象的克隆,可以通过复制集合中对应的原型对象来获得。
原型管理器标准实现:public class PrototypeManager { private Hashtable<String,DarkWeeklReport> hashtable = new Hashtable(); // 使用HashTable存储原型对象,线程安全 public PrototypeManager() { DarkWeeklReport defaultWeekly = new DarkWeeklReport(); defaultWeekly.setName("默认周报模板"); defaultWeekly.setDate("2019.12.02"); defaultWeekly.setContent("默认内容"); DarkAttachment defaultAttachment = new DarkAttachment(); defaultAttachment.setName("默认附件"); defaultWeekly.setAttachment(defaultAttachment); hashtable.put("default",defaultWeekly); } public void add(String key, DarkWeeklReport darkWeeklReport){ hashtable.put(key,darkWeeklReport); } public DarkWeeklReport get(String key) throws IOException, ClassNotFoundException { return hashtable.get(key).deepClone(); } }
客户端测试代码:
public static void main(String[] args) throws Exception { // 浅克隆实现,修改下clone出的周报的附件,原型周报的附件也会更改,但是我们要求附件等属性也是可以更改的,所以需要进行深克隆 //simpleClone(); // 使用序列化从头实现深克隆 //darkClone(); // 测试下原型管理器 testPrototypeManager(); } private static void testPrototypeManager() throws IOException, ClassNotFoundException { PrototypeManager manager = new PrototypeManager(); DarkWeeklReport darkWeeklReport = manager.get("default"); System.out.println(darkWeeklReport); }
结果:
-
优缺点分析
-
优点
- 创建新的对象实例较为复杂时,可以简化对象的创建过程,通过复制一个已有的实例可以提高新实例的创建效率;
- 提供了抽象原型类,可扩展性较好;
- 直接通过原型类中的clone方法就可以实现创建新的对象,简化了创建步骤,无需像工厂模式那样添加大量的类;
- 可以利用原型模式来存储对象的状态,将某一时刻的对象克隆一份存储起来,然后在想要进行撤销操作时用该对象来恢复。
-
缺点
- 每一个具体的原型类都需要配备一个克隆方法,而且该克隆方法位于类的内部,当需要对已有类进行修改时需要修改源代码,违背了开闭原则。
- 实现深克隆时需要编写较为复杂的代码,当对象直接存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类对必须支持深克隆,实现起来会比较麻烦。
-
适用环境
在以下情况下可以考虑使用原型模式:
1. 创建新对象的成本比较大(例如初始化很慢、占用太多的CPU资源或网络资源),新对象可以通过复制已有对象来获得,如果是相似对象,可以对其成员变量稍作修改;
2. 系统要保存对象的状态,而对象的状态变化很小;
3. 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象的带新实例可能比使用构造函数创建一个新实例更加方便。
-
-
自练习习题
- 某公司需要创建一个公文管理器,公文管理器中需要提供一个集合对象来存储一些公文模板,用户可以通过复制这些模板快速的创建新的公文,试使用带有公文管理器的原型模式来设计该公文管理器(请认真完成)。
自律即自由,认真编写每一行代码!!!
半原创博客,用以记录学习,希望可以帮到您,不喜可喷。