设计模式之原型模式(Prototype)
1.原型模式的概念
用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。
2.原型模式的结构图
Prototype(原型类):声明一个克隆自身的类或接口,这个类或接口通常会实现Cloneable接口。
ConcretePrototype(具体原型类):被复制的对象,实现一个克隆自身的操作。
Client(客户端):让一个原型克隆自身,从而创建一个新的对象。
3.原型模式的例子
举《大话设计模式》中的例子,感觉很具有代表性:需求是需要写一个输出简历信息的代码,简历的具体格式包括某个人,这个人的性别年龄,以及这个人在各个时间段就职的公司。如果不使用设计模式的话该如何做呢?
不使用设计模式的代码示例:
Resume(简历类):
package com.jxs.prototype;
/**
* Created by jiangxs on 2018/5/3.
*
* 简历类
*/
public class Resume {
private String name;
private String sex;
private int age;
private String workDate;
private String company;
public Resume(String name) {
this.name = name;
work = new WorkExperience();
}
// 设置个人信息
public void setPersonalInfo(String sex, int age) {
this.sex = sex;
this.age = age;
}
// 设置工作经历
public void setWorkExperience(String workDate, String company){
work.setWorkDate(workDate);
work.setCompany(company);
}
// 显示
public void display() {
System.out.println(name + " " + sex + " " + age);
System.out.println("工作经历:" + workDate + " " + company);
}
}
客户端:
package com.jxs.prototype;
/**
* Created by jiangxs on 2018/5/3.
*/
public class PrototypeClient {
public static void main(String[] args) throws CloneNotSupportedException {
Resume a = new Resume("大鸟");
a.setPersonalInfo("男", 29);
a.setWorkExperience("2015-2016", "xxx公司");
Resume b = new Resume("大鸟");
b.setPersonalInfo("男", 29);
b.setWorkExperience("2016-2017", "yyy公司");
Resume c = new Resume("大鸟");
c.setPersonalInfo("男", 29);
c.setWorkExperience("2017-2018", "zzz公司");
a.display();
b.display();
c.display();
}
}
运行结果:
大鸟 男 29
工作经历:2015-2016 xxx公司
大鸟 大鸟 29
工作经历:2016-2017 yyy公司
大鸟 男 29
工作经历:2017-2018 zzz公司
Process finished with exit code 0
虽然说上面的代码可以完成需求,但是如果时间段有100个,那么就要像这样实例化出100个对象,这样做不仅操作麻烦,并且也会占用大量的资源。这时候就可以使用原型设计模式来很好的解决这个问题。
原型设计模式就是从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节。
下面使用原型设计模式来完成上面例子当中的需求:
Resume(ConcretePrototype):
package com.jxs.prototype;
/**
* Created by jiangxs on 2018/5/3.
*
* 简历类
*/
public class Resume {
private String name;
private String sex;
private int age;
private String workDate;
private String company;
public Resume(String name) {
this.name = name;
work = new WorkExperience();
}
// 设置个人信息
public void setPersonalInfo(String sex, int age) {
this.sex = sex;
this.age = age;
}
// 设置工作经历
public void setWorkExperience(String workDate, String company){
work.setWorkDate(workDate);
work.setCompany(company);
}
// 显示
public void display() {
System.out.println(name + " " + sex + " " + age);
System.out.println("工作经历:" + workDate + " " + company);
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
客户端:
package com.jxs.prototype;
/**
* Created by jiangxs on 2018/5/3.
*/
public class PrototypeClient {
public static void main(String[] args) throws CloneNotSupportedException {
Resume a = new Resume("大鸟");
a.setPersonalInfo("男", 29);
a.setWorkExperience("2016-2017", "xxx公司");
Resume b = (Resume) a.clone();
b.setWorkExperience("2017-2018", "yyy公司");
Resume c = (Resume) a.clone();
c.setPersonalInfo("男", 24);
a.display();
b.display();
c.display();
}
}
运行结果:
大鸟 男 29
工作经历:2015-2016 xxx公司
大鸟 大鸟 29
工作经历:2016-2017 yyy公司
大鸟 男 24
工作经历:2015-2016 xxx公司
Process finished with exit code 0
4.深复制与浅复制
我们通过克隆的方式就可以轻松的完成新简历的生成,并且可以再修改新简历的细节。但是当我们的Resume类中有对象的引用时,引用的对象数据是否可以引用过来呢?假设增加一个工作经历类,将时间区间和公司名称等属性放入该类中。
WorkExperience:
package com.jxs.prototype;
/**
* Created by jiangxs on 2018/5/3.
*
* 工作经历类
*/
public class WorkExperience implements Cloneable {
private String workDate;
private String company;
public String getWorkDate() {
return workDate;
}
public void setWorkDate(String workDate) {
this.workDate = workDate;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// WorkExperience类实现克隆方法
return super.clone();
}
}
Resume:
package com.jxs.prototype;
/**
* Created by jiangxs on 2018/5/3.
*
* 简历类
*/
public class Resume implements Cloneable {
private String name;
private String sex;
private int age;
private WorkExperience work;
public Resume(String name) {
this.name = name;
work = new WorkExperience();
}
private Resume(WorkExperience work) throws CloneNotSupportedException {
// 提供clone方法调用的私有构造函数,以便克隆WorkExperience的数据
this.work = (WorkExperience) work.clone();
}
// 设置个人信息
public void setPersonalInfo(String sex, int age) {
this.sex = sex;
this.age = age;
}
// 设置工作经历
public void setWorkExperience(String workDate, String company){
work.setWorkDate(workDate);
work.setCompany(company);
}
// 显示
public void display() {
System.out.println(name + " " + sex + " " + age);
System.out.println("工作经历:" + work.getWorkDate() + " " + work.getCompany());
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
客户端:
package com.jxs.prototype;
/**
* Created by jiangxs on 2018/5/3.
*/
public class PrototypeClient {
public static void main(String[] args) throws CloneNotSupportedException {
Resume a = new Resume("大鸟");
a.setPersonalInfo("男", 29);
a.setWorkExperience("2015-2016", "xxx公司");
Resume b = (Resume) a.clone();
b.setWorkExperience("2016-2017", "yyy公司");
Resume c = (Resume) a.clone();
c.setPersonalInfo("男", 22);
b.setWorkExperience("2017-2018", "zzz公司");
a.display();
b.display();
c.display();
}
}
运行结果:
大鸟 男 29
工作经历:2017-2018 zzz公司
大鸟 大鸟 29
工作经历:2017-2018 zzz公司
大鸟 男 24
工作经历:2017-2018 zzz公司
Process finished with exit code 0
我们发现最终的结果是工作经历都是相同的,也就是说在克隆时针对引用类型时,只是复制了引用,而对引用的对象还是指向了原来的对象,所以就会出现给a、b、c三个引用设置‘工作经历’,但却同时看到三个引用都是最后一次设置,因为三个引用都指向了同一个对象。所以clone()方法就是这样:如果Resume对象中的数据类型是值类型的,则对该字段执行逐位复制;如果数据类型是引用类型,则复制引用但不复制引用的对象,因此,原始对象及其副本引用同一对象。
上面所说的现象其实就是‘浅复制’。下面就来说说浅复制和深复制。
浅复制:被复制对象的所有变量都含有与原来对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。
深复制:深复制把引用对象的变量指向复制过的新对象,而不是原有的被引用的对象。
下面我们看看深复制和浅复制的一个简单的图就很明了了。
浅复制:
深复制:
所以要想上面的代码达到最终三次显示的结果各不相同,就要使用深复制来进行实现:
WorkExperience的代码上面已经写过,这里不再重新写了。
Resume:
package com.jxs.prototype;
/**
* Created by jiangxs on 2018/5/3.
*
* 简历类
*/
public class Resume implements Cloneable {
private String name;
private String sex;
private int age;
private WorkExperience work;
public Resume(String name) {
this.name = name;
work = new WorkExperience();
}
private Resume(WorkExperience work) throws CloneNotSupportedException {
// 提供clone方法调用的私有构造函数,以便克隆WorkExperience的数据
this.work = (WorkExperience) work.clone();
}
// 设置个人信息
public void setPersonalInfo(String sex, int age) {
this.sex = sex;
this.age = age;
}
// 设置工作经历
public void setWorkExperience(String workDate, String company) {
work.setWorkDate(workDate);
work.setCompany(company);
}
// 显示
public void display() {
System.out.println(name + " " + sex + " " + age);
System.out.println("工作经历:" + work.getWorkDate() + " " + work.getCompany());
}
@Override
protected Object clone() throws CloneNotSupportedException {
// 调用私有的构造方法,让WorkExperience克隆完成
// 然后再给这个Resume对象的相关字段赋值
// 最终返回一个深复制的简历对象
Resume obj = new Resume(this.work);
obj.name = this.name;
obj.sex = this.name;
obj.age = this.age;
return obj;
}
}
客户端代码使用上面的客户端代码,这里也不再重复。
下面看运行结果:
大鸟 男 29
工作经历:2015-2016 xxx公司
大鸟 大鸟 29
工作经历:2016-2017 yyy公司
大鸟 男 24
工作经历:2017-2018 zzz公司
5.原型设计模式的总结
(1)原型模式的优点:
①当创建对象的实例较为复杂的时候,使用原型模式可以简化对象的创建过程,通过复制一个已有的实例可以提高实例的创建效率。
②扩展性好,由于原型模式提供了抽象原型类,在客户端针对抽象原型类进行编程,而将具体原型类写到配置文件中,增减或减少产品对原有系统都没有影响。
③原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式不需要这样,圆形模式中产品的复制是通过封装在类中的克隆方法实现的,无需专门的工厂类来创建产品。
④可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
(2)原型模式的缺点:
①需要为每一个类配置一个克隆方法,而且该克隆方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
②在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重签到引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。
(3)原型模式的适用环境:
①创建新对象成本较大(例如初始化时间长,占用CPU多或占太多网络资源),新对象可以通过复制已有对象来获得,如果相似对象,则可以对其成员变量稍作修改。
②系统要保存对象的状态,而对象的状态很小。
③需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的组合状态,通过复制原型对象得到新实例可以比使用构造函数创建一个新实例更加方便。
注:以上代码均可在github上进行下载:https://github.com/xsongj/designPattern
参考:《大话设计模式》