设计模式-原型模式-Java
目录
文章目录
内容
1、示例案例-工作周报
- 大同小异的工作周报
Sunny软件公司一直使用自行开发的一套OA (Office Automatic,办公自动化)系统进行日常工作办理,但在使用过程中,越来越多的人对工作周报的创建和编写模块产生了抱怨。追其原因,Sunny软件公司的OA管理员发现,由于某些岗位每周工作存在重复性,工作周报内容都大同小异,如图7-1工作周报示意图。这些周报只有一些小地方存在差异,但是现行系统每周
默认创建的周报都是空白报表,用户只能通过重新输入或不断复制粘贴来填写重复的周报内容,极大降低了工作效率,浪费宝贵的时间。如何快速创建相同或者相似的工作周报,成为Sunny公司OA开发人员面临的一个新问题。
图1-1 工作周报示意图
- 设计和实现
Sunny公司的开发人员通过对问题进行仔细分析,决定按照如下思路对工作周报模块进行重新设计和实现:
- (1)除了允许用户创建新周报外,还允许用户将创建好的周报保存为模板;
- (2)用户在再次创建周报时,可以创建全新的周报,还可以选择合适的模板复制生成一份相同
的周报,然后对新生成的周报根据实际情况进行修改,产生新的周报。
只要按照如上两个步骤进行处理,工作周报的创建效率将得以大大提高。这个过程让我到平时经常进行的两个电脑基本操作:复制和粘贴,快捷键通常为Ctrl + C和Ctrl + V,已有对象的复制和粘贴,我们可以创建大量的相同对象。如何在一个面向对象系统中实象的复制和粘贴呢?不用着急,本章我们介绍的原型模式正为解决此类问题而诞生。
2、原型模式概述
在使用原型模式时,我们需要首先创建一个原型对象,再通过复制这个原型对象来创建更多同类型的对象。试想,如果连孙悟空的模样都不知道,怎么拔毛变小猴子呢?
2.1、原型模式定义
原型模式(Prototype Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。
2.2、原型模式原理
将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。由于在软件系统中我们经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在真实开发中的使用频率还是非常高的。原型模式是一种“另类”的创建型模式,创建克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。
需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式修改可以得到一系列相似但不完全相同的对象。
2.3、原型模式结构图中角色
图2.3 原型模式结果图
- Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至可以说具体实现类。
- ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
- Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,在通过调用搞对象的克隆方法即可得到多个相同的对象。由于客户端针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
3、克隆方法实现
原型模式的核心在于如何实现克隆方法,下面将介绍两种在Java语言中常用的克隆实现方法:
3.1、通用实现方法
通用的克隆实现方法是在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其返回,并将相关的参数传入新创建的对象中,保证它们的成员属性相同。
-
原型类代码3.1-1如下所示:
class ConcretePrototype implements Prototype { private String attr; //成员属性 public void setAttr(String attr) { this.attr = attr; } public String getAttr() { return this.attr; } public Prototype clone() //克隆方法 { Prototype prototype = new ConcretePrototype(); //创建新对象 prototype.setAttr(this.attr); return prototype; } }
-
测试类代码3.1-2如下
Prototype obj1 = new ConcretePrototype(); obj1.setAttr("Sunny"); Prototype obj2 = obj1.clone();
这种方法可作为原型模式的通用实现,它与编程语言特性无关,任何面向对象语言都可以使用这种形式来实现对原型的克隆。
3.2、Java语言提供的clone()方法
学过Java语言的人都知道,所有的Java类都继承自java.lang.Object。事实上,Object类提供一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。
需要注意的是能够实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedException异常。
-
原型父接口代码3.2-1如下:
package prototype; // 原型接口 public interface Prototype { Prototype clone(); }
-
具体原型类代码3.2-2如下:
package prototype; public class ConcretePrototype implements Prototype, Cloneable{ public ConcretePrototype() {} @Override public Prototype clone() { Object object = null; try { object = super.clone(); } catch (CloneNotSupportedException e) { System.err.println("Not support cloneable"); } System.out.println("通过原型对象克隆方法克隆新对象"); return (Prototype)object; } }
-
测试类代码3.2-3如下:
package prototype; public class TestPrototype { public static void main(String[] args) { Prototype type = new ConcretePrototype(); Prototype t1 = type.clone(); Prototype t2 = type.clone(); System.out.println(type == t1); System.out.println(type == t2); System.out.println(t1 == t2); } }
-
测试结果:
通过原型对象克隆方法克隆新对象 通过原型对象克隆方法克隆新对象 false false false
4、Java的clone()方法
一般而言,Java语言中的clone()方法满足:
- (1) 对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;
- (2) 对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一样;
- (3) 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。
为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:
- (1) 在派生类中覆盖基类的clone()方法,并声明为public;
- (2) 在派生类的clone()方法中,调用super.clone();
- (3)派生类需实现Cloneable接口。
此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类
5、工作周报(原型模式)完整解决方案
Sunny公司开发人员决定使用原型模式来实现工作周报的快速创建,快速创建工作周报结构图 如图5-1所示:
图5-1快速创建工作报告结构图
在图5-1中,WeeklyLog充当具体原型类,Object类充当抽象原型类,clone()方法为原型方法。
-
工作周报WeeklyLog类:具体原型类代码5-1如下
package prototype; public class WeeklyLog implements Cloneable{ private String name; private String date; private String content; 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; } @Override public WeeklyLog clone() { Object obj = null; try { obj = super.clone(); return (WeeklyLog) obj; } catch(CloneNotSupportedException e) { System.out.println("不支持复制!"); return null; } } }
-
测试类代码5-2如下:
package prototype; public class TestWeeklyLog { public static void main(String[] args) { WeeklyLog log_previous = new WeeklyLog(); //创建原型对象 log_previous.setName("张无忌"); log_previous.setDate("第12周"); log_previous.setContent("这周工作很忙,每天加班!"); System.out.println("****周报****"); System.out.println("周次:" + log_previous.getDate()); System.out.println("姓名:" + log_previous.getName()); System.out.println("内容:" + log_previous.getContent()); System.out.println("--------------------------------"); WeeklyLog log_new; log_new = log_previous.clone(); //调用克隆方法创建克隆对象 log_new.setDate("第13周"); System.out.println("****周报****"); System.out.println("周次:" + log_new.getDate()); System.out.println("姓名:" + log_new.getName()); System.out.println("内容:" + log_new.getContent()); System.out.println("--------------------------------"); System.out.println(log_previous == log_new); System.out.println(log_previous.getDate() == log_new.getDate()); System.out.println(log_previous.getName() == log_new.getName()); System.out.println(log_previous.getContent() == log_new.getContent()); } }
-
测试结果:
****周报**** 周次:第12周 姓名:张无忌 内容:这周工作很忙,每天加班! -------------------------------- ****周报**** 周次:第13周 姓名:张无忌 内容:这周工作很忙,每天加班! -------------------------------- false false true true
通过已创建的工作周报可以快速创建新的周报,然后再根据需要修改周报,无须再从头开始创建。原型模式为工作流系统中任务单的快速生成提供了一种解决方案。
6、浅克隆和深克隆
6.1、带附件的周报
通过引入原型模式,Sunny软件公司OA系统支持工作周报的快速克隆,极大提高了工作周报的编写效率,受到员工的一致好评。但有员工又发现一个问题,有些工作周报带有附件,例如经理助理“小龙女”的周报通常附有本周项目进展报告汇总表、本周客户反馈信息汇总表等,如果使用上述原型模式来复制周报,周报虽然可以复制,但是周报的附件并不能复制,这是
由于什么原因导致的呢?如何才能实现周报和附件的同时复制呢?
在回答这些问题之前,先介绍一下两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类
型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进
行详细介绍。
6.2、浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本
身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制,如图6.2-1所示:
图6.2-1 浅克隆示意图
在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。为了让大家更好地理解浅克隆和深克隆的区别,我们首先使用浅克隆来实现工作周报和附件类的复制,其结构如图6.2-2所示:
图6.2-2 带附件的周报结构图(浅克隆)
-
附件类代码6.2-1如下:
package prototype; 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("下载附件,文件名为:" + this.name); } }
-
带附件周报类代码6.2-2如下:
package prototype; public class ShallowCloneWeeklyLog implements Cloneable{ private Attachment attachment; private String name; private String date; private String content; public ShallowCloneWeeklyLog() {} public ShallowCloneWeeklyLog(Attachment attachment, String name, String date, String content) { super(); this.attachment = attachment; this.name = name; this.date = date; this.content = 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()方法实现浅克隆 public ShallowCloneWeeklyLog clone() { Object obj = null; try { obj = super.clone(); return (ShallowCloneWeeklyLog)obj; } catch(CloneNotSupportedException e) { System.out.println("不支持复制!"); return null; } } }
-
测试类代码6.2-3如下:
package prototype; public class TestShallowCloneWeeklyClone { public static void main(String[] args) { ShallowCloneWeeklyLog log_previous, log_new; log_previous = new ShallowCloneWeeklyLog(); //创建原型对象 Attachment attachment = new Attachment(); //创建附件对象 log_previous.setAttachment(attachment); //将附件添加到周报中 log_new = log_previous.clone(); //调用克隆方法创建克隆对象 //比较周报 System.out.println("周报是否相同? " + (log_previous == log_new)); //比较附件 System.out.println("附件是否相同? " + (log_previous.getAttachment() == log_new.getAttachment())); } }
-
测试结果:
周报是否相同? false 附件是否相同? true
由于使用的是浅克隆技术,因此工作周报对象复制成功,通过“==”比较原型对象和克隆对象的内存地址时输出false;但是比较附件对象的内存地址时输出true,说明它们在内存中是同一个对象。
6.3、深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制,如图6.3-1所示:
图6.3-1 深克隆示意图
在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能
够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。下面我们使用深克隆技术来实现工作周报和附件对象的复制,由于要将附件对象和工作周报对象都写入流中,因此两个类均需要实现Serializable接口,其结构如图6.3-2所示:
图6.3-2 带附件的周报结构图(深克隆)
-
附件类代码6.3-1:
package prototype; import java.io.Serializable; public class Attachment implements Serializable{ private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public void download() { System.out.println("下载附件,文件名为:" + this.name); } }
-
带附件的周报类(深克隆)代码6.3-2:
package prototype; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class DeepCloneWeeklyLog implements Serializable { private Attachment attachment; private String name; private String date; private String content; public DeepCloneWeeklyLog() {} public DeepCloneWeeklyLog(Attachment attachment, String name, String date, String content) { super(); this.attachment = attachment; this.name = name; this.date = date; this.content = 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; } // 使用序列化技术实现深克隆 public DeepCloneWeeklyLog deepClone() throws IOException, ClassNotFoundException { // 把对象写入流中 ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(this); // 把对象从流中读出 ByteArrayInputStream bai = new ByteArrayInputStream(bao.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bai); return (DeepCloneWeeklyLog)ois.readObject(); } }
-
测试类代码6.3-3:
package prototype; public class TestDeepCloneWeeklyLog { public static void main(String[] args) { DeepCloneWeeklyLog log_previous, log_new = null; log_previous = new DeepCloneWeeklyLog(); //创建原型对象 Attachment attachment = new Attachment(); //创建附件对象 log_previous.setAttachment(attachment); //将附件添加到周报中 try { log_new = log_previous.deepClone(); //调用深克隆方法创建克隆对象 } catch(Exception e) { System.err.println("克隆失败!"); } //比较周报 System.out.println("周报是否相同? " + (log_previous == log_new)); //比较附件 System.out.println("附件是否相同? " + (log_previous.getAttachment() == log_new.getAttachment())); } }
-
测试结果:
周报是否相同? false 附件是否相同? false
从输出结果可以看出,由于使用了深克隆技术,附件对象也得以复制,因此“==”比较原型对象的附件和克隆对象的附件时输出结果均为false。深克隆技术实现了原型对象和克隆对象的完全独立,对任意克隆对象的修改都不会给其他对象产生影响,是一种更为理想的克隆实现方式。
6.4、扩展
Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的
实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。
7、原型管理器
7.1、原型管理器引入和实现
原型管理器(Prototype Manager)是将多个具有某种相关性原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。在原型管理器中针对抽象原型类进行编程,以便扩展。其结构如图7.1-1所示:
图7.1-1 带原型管理器的原型模式
下面通过模拟一个简单的公文管理器来介绍原型管理器的设计与实现: Sunny软件公司在日常办公中有许多公文需要创建、递交和审批,例如《可行性分析报告》、《立项建议书》、《软件需求规格说明书》、《项目进展报告》等,为了提高工作效率,在OA系统中为各类公文均创建了模板,用户可以通过这些模板快速创建新的公文,这些公文模板需要统一进行管
理,系统根据用户请求的不同生成不同的新公文。
我们使用带原型管理器的原型模式实现公文管理器的设计,其结构如图7.1-2所示:
图7.1-2 公文管理器结构图
-
抽象公文管理器接口代码7.1-1:
package prototype; // 公文接口 public interface OfficialDocument extends Cloneable{ public OfficialDocument clone(); // 克隆 public void display(); // 显示信息 }
-
可行性分析报告FAR(Feasibility Analysis Report)类代码7.1-2:
package prototype; public class FAR implements OfficialDocument{ public OfficialDocument clone() { OfficialDocument far = null; try { far = (OfficialDocument) super.clone(); } catch(CloneNotSupportedException e) { System.out.println("不支持复制"); } return far; } public void display() { System.out.println("《可行性分析报告》"); } }
-
软件需求规格说明书SRS(Software Requirements Specification)类代码7.1-3:
package prototype; public class SRS implements OfficialDocument { public OfficialDocument clone() { OfficialDocument srs = null; try { srs = (OfficialDocument) super.clone(); } catch(CloneNotSupportedException e) { System.out.println("不支持复制"); } return srs; } public void display() { System.out.println("《软件需求规格说明书》"); } }
-
原型管理器(使用IoDH单例模式实现)类代码7.1-4:
package prototype; import java.util.HashMap; // IoDH单例模式实现的公文管理器类 public class PrototypeManager { // 存储原型对象 private HashMap<String, OfficialDocument> hm = new HashMap<>(); // 私有内部类,延迟生成唯一公文管理器对象 private static class HolderClass { private final static PrototypeManager instance = new PrototypeManager(); } // 添加初始化公文 private PrototypeManager() { hm.put("far",new FAR()); hm.put("srs",new SRS()); } // 添加新公文 public void addOfficialDocument(String key, OfficialDocument doc) { hm.put(key, doc); } // 通过浅克隆获取新的公文对象 public OfficialDocument getOfficialDocument(String key) { return ((OfficialDocument)hm.get(key)).clone(); } // 获取唯一公文管理器对象 public static PrototypeManager getPrototypeManager() { return HolderClass.instance; } }
-
测试类7.1-4:
package prototype; public class TestPrototypeManger { public static void main(String[] args) { //获取原型管理器对象 PrototypeManager pm = PrototypeManager.getPrototypeManager(); OfficialDocument doc1,doc2,doc3,doc4; doc1 = pm.getOfficialDocument("far"); doc1.display(); doc2 = pm.getOfficialDocument("far"); doc2.display(); System.out.println(doc1 == doc2); doc3 = pm.getOfficialDocument("srs"); doc3.display(); doc4 = pm.getOfficialDocument("srs"); doc4.display(); System.out.println(doc3 == doc4); } }
-
测试结果7.1-1:
《可行性分析报告》 《可行性分析报告》 false 《软件需求规格说明书》 《软件需求规格说明书》 false
在PrototypeManager中定义了一个HashMap类型的集合对象,使用“键值对”来存储原型对象,客户端可以通过Key(如“far”或“srs”)来获取对应原型对象的克隆对象。PrototypeManager类提供了类似工厂方法的getOfficialDocument()方法用于返回一个克隆对象。在本实例代码中,我们将PrototypeManager设计为单例类,使用IoDH单例实现,确保系统中有且仅有一个PrototypeManager对象,有利于节省系统资源,并可以更好地对原型管理器对象进行控制。
8、原型模式总结
原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中应用较为广泛,很多软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操作就是原型模式的典型应用,下面对该模式的使用效果和适用情况进行简单的总结。
8.1、优缺点
-
主要优点:
- (1) 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个
已有实例可以提高新实例的创建效率。 - (2) 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编
程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。 - (3) 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的
工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。 - (4) 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起
来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
- (1) 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个
-
主要缺点:
- (1) 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的行改造时,需要修改源代码,违背了“开闭原则”。
- (2) 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻
8.2、适用场景
- (1) 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
- (2) 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
- (3) 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的
几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。
后记 :
本项目为参考某马视频开发,相关视频及配套资料可自行度娘或者联系本人。上面为自己编写的开发文档,持续更新。欢迎交流,本人QQ:806797785
前端项目源代码地址:https://gitee.com/gaogzhen/vue-leyou
后端JAVA源代码地址:https://gitee.com/gaogzhen/JAVA