漫谈设计模式(四)—— 原型模式

1.前言

原型模式是创建型设计模式之一。本来已经决定不写相关部分的内容,因为其内容并不是很难。

后来想了一下,里面涉及到的思考点和细节点比较多,还是应该总结一下。其次,设计模式本身可能并不会很难,哪怕是对花点时间多去看一些例子,总归能看懂,但是设计模式之难不在其本身,难在其应用场景,你可能知道它是怎样实现的,但是你不知道它应该在什么场合下被使用。使用好了它有益于系统,而滥用则会为系统增加复杂度和冗余度,它考察编程人员举一反三的能力和设计思维。

此外,原型模式也算是相对比较常见常用的设计模式之一。

基于此,还是将其总结于此,权当是对自己认知的总结,也作为督促自己的一部分。

2.从需求说起

本博客写于2020年5月,新型冠状病毒还是当下重中之重。假设现在你住在某省某市某小区xx东xx单元xx号,需要每天通过网络填写一份表单向当地社区报备体温情况。表单里需要填写的内容包括:身份证号、姓名、住址、温度。

下面开始实现。
(1)表单结构,地址是一个对象,包含省、市、街道和详细地址。

package prototype;

public class Report {
	String id;
	String name;
	double temperature;
	Address adress;
	//getter和setter、toString省略
	
	class Address{
		String province;
		String city;
		String road;
		String detail;
		//getter和setter、toString省略
	}
}

(2)开始填写表单。此处假设打印表单对象即为提交。

package prototype;
import java.util.Date;
import prototype.Report.Address;

public class Test1 {
	public static void main(String[] args) {
		//第一天填写
		Report first = new Report();
		first.setId("310……");
		first.setName("Dolphin");
		Address address1 = first.new Address();
		address1.setProvince("SH");
		address1.setCity("SH");
		address1.setRoad("Rd Nandan");
		address1.setDetail("xx community xx building xx unit No.xx");
		first.setAdress(address1);
		first.setTemperature(36.2);
		System.out.println(first);
		
		//第二天填写
		Report second = new Report();
		second.setId("310……");
		second.setName("Dolphin");
		Address address2 = first.new Address();
		address1.setProvince("SH");
		address1.setCity("SH");
		address1.setRoad("Rd Nandan");
		address1.setDetail("xx community xx building xx unit No.xx");
		second.setAdress(address1);
		second.setTemperature(36.3);
		System.out.println(second);
		//……
	}
}

2.1 问题

上述实现方法有什么问题吗?挺好的啊~

从结果来看,上述实现并没有什么问题。

但是,仔细分析一下这个需求。一个人,需要向社区每日报备体温。在这个需求中,其实每天填写时需要修改的只有体温这个参数

但是上述实现过程中,这个人其实每天都把身份证号、姓名、地址这些信息填了一遍又一遍,烦不烦?

烦!假如这个人又是一个对电子产品一窍不通的人,那每天经历这个过程可谓是太煎熬了

2.2 解决方案设想

针对上面提出的问题,有没有什么解决方案呢?

最好的解决方案就是,第一天填写了之后,能够保存相关信息,就相当于登录网页时的记录用户名和密码。

在下一次填写时,把相关信息加载出来,再把需要改的改掉就行了。

这应该算的上一种比较好的解决方式。

也许还有更好的解决方式需要去发现去思考,但是为了引出原型模式,这个解决方案是必须的。

3. 原型模式

3.1 简介

前面也说到了,原型模式是创建型设计模式之一,它是用来描述如何创建对象的,更确切的来说,结合上面这个需求,它是用来描述如何创建第二个、第三个……第N个与第一个对象很相似的对象。

从系统的角度上来说,这有点类似系统中的Ctrl+CCtrl+V,用于创建重复对象。
注意,这里说的是类似,而不是等同。具体哪里不同,后文会说。

在这里引申出一个概念:原型。用上面的例子来说,第一天的表单对象就是一个原型;用系统复制粘贴来说,你复制的东西就是原型。通过这个原型创建出第二个、第三个……第N个与原型对象很相似的对象,这就是原型模式的雏形。

原型模式的实现方法很简单。让原型对象类型实现Cloneable接口,并重写Object.clone()方法即可。

听起来是不是很简单,那就一步步实现吧。

3.1 原型模式初体验——浅拷贝

package prototype;

public class Report implements Cloneable{
	String id;
	String name;
	double temperature;
	Address adress;
	//getter和setter、toString省略

	//建议把protected改成public,使得可以在包外使用。也可以使用多态中的协变将返回Object修改为返回所需克隆的对象(此例中即为Report)
	@Override
	protected Object clone() throws CloneNotSupportedException {
		return super.clone();
	}

	class Address{
		String province;
		String city;
		String road;
		String detail;
		//getter和setter、toString省略
	}
}
package prototype;
import prototype.Report.Address;

public class Test2 {
	public static void main(String[] args) throws CloneNotSupportedException {
		Report first = new Report();
		first.setId("310……");
		first.setName("Dolphin");
		Address address1 = first.new Address();
		address1.setProvince("SH");
		address1.setCity("SH");
		address1.setRoad("Rd Nandan");
		address1.setDetail("xx community xx building xx unit No.xx");
		first.setAdress(address1);
		first.setTemperature(36.2);
		System.out.println(first);
		
		Report second = (Report) first.clone();
		second.setTemperature(36.3);
		System.out.println(second);	
	}
}

输出

Report [id=310……, name=Dolphin, temperature=36.2, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]

可以看到除了温度有所变化之外(用黄色标了出来),其余均没有变化。通过克隆原型对象,一些重复的信息就不需要一而再再而三的填了。这就是原型设计模式的好处,其功能类似于前文所说的把原型对象保存起来,后面需要用的时候再加载。

3.2 思考

从面向对象的角度来看,我们需要弄清楚几个问题:

  1. 既然是克隆,那么原型对象和克隆出来的对象是不是同一个对象?
  2. 修改其中之一,会不会影响到另外一个?

首先,验证第一个问题。输出两个对象的散列码,看看两者是否相等;也可以直接判断二者是否相等。

System.out.println(first.hashCode());
System.out.println(second.hashCode());
System.out.println(first == second);

输出

2018699554
1311053135
false

从输出可以看到,不管是散列码还是判断相等,都说明原型对象和克隆出来的对象并不属于同一个,而是指向了不同的地址空间。

再验证第二个问题。

		//原型对象创建过程略
		System.out.println("---------------------------unmodified------------------------------");
		System.out.println("prototype: " + first);
		second.setTemperature(36.3);
		System.out.println("clone: " + second);
		
		System.out.println("---------------------------prototype modified 1------------------------------");
		first.setName("Thaksin");
		System.out.println("prototype after prototype modified 1: " + first);
		System.out.println("clone after prototype modified 1: " + second);
		
		System.out.println("---------------------------prototype modified 2------------------------------");
		first.getAdress().setCity("SSSS");
		System.out.println("prototype after prototype modified 2: " + first);
		System.out.println("clone after prototype modified 2: " + second);
		
		
		System.out.println("---------------------------clone modified 1------------------------------");
		second.setId("450……");
		System.out.println("prototype after clone modified 1: " + first);
		System.out.println("clone after clone modified 1: " + second);
		
		System.out.println("---------------------------clone modified 2------------------------------");
		second.getAdress().setCity("NN");
		System.out.println("prototype after clone modified 2: " + first);
		System.out.println("clone after clone modified 2: " + second);

输出如下(修改的字段用黄颜色标出,便于比较)

---------------------------unmodified------------------------------
prototype: Report [id=310……, name=Dolphin, temperature=36.2, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------prototype modified 1------------------------------
prototype after prototype modified 1: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after prototype modified 1: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------prototype modified 2------------------------------
prototype after prototype modified 2: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after prototype modified 2: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------clone modified 1------------------------------
prototype after clone modified 1: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after clone modified 1: Report [id=450……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------clone modified 2------------------------------
prototype after clone modified 2: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=NN, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after clone modified 2: Report [id=450……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=NN, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]

从输出中,对比上下两个对象的高亮部分,不难看出,不管是修改原型对象还是修改克隆出来的对象,当修改的字段为基本类型和String时,不会影响另外一个;而当修改的内容为类的引用(如Address addressDate date等)时,就会将另一个对象里的字段也修改了

这是为什么呢?也许下面这张图可以解答这个疑惑。

在这里插入图片描述
看懂这张图需要懂一定的java创建对象与内存分配的知识。new Report()生成原型对象,在堆内开辟了一块内存,该内存的首地址为0xfac(假设),而这个原型对象中组合了Address对象,所以在堆内存中又开辟了一块内存(假设首地址为0xaaa),则原型对象中address这个引用指向这块内存的首地址。另外,栈中存放new Report()这个原型对象的引用first,该引用指向堆中0xfac这块内存。

clone 操作 将 原型对象在堆中原样复制了一份,并分配了不同的地址(假设为0xfab),栈中second为复制所得的对象的引用。由于是原样复制,所以克隆对象中的address引用同样指向0xaaa这块内存

clone的这种原样复制的方式叫做浅拷贝

浅拷贝机制使得两个对象中的address引用都指向同一块内存,因此当其中一个对象对这块内存中的信息做了更改后,由于另外一个对象也指向这个内存,所以同样也会接收到这个更改。

说到这里,说明一下前文提及的浅拷贝和系统中Ctrl+CCtrl+V的区别:在浅拷贝机制下,当对象中存在对象引用时,修改其中一个对象中引用的内容,会影响另外一个;而系统中的复制粘贴不会存在修改一个影响另外一个的情况。

3.3 改进——深拷贝

有浅拷贝,就会有深拷贝。

深拷贝,即在clone函数中,对原型对象中的对象引用(address)也进行拷贝,这样原型对象和克隆所得的对象中的对象引用(address)就可以指向不同的内存区域,使得修改其中一个引用不会再引起另外一个引用被修改。

//修改Address类,让其实现Cloneable,并重写Object.clone()方法
class Address implements Cloneable{
		//略……
		@Override
		public Object clone() throws CloneNotSupportedException {
			Address d = null;
			//   try/catch CloneNotSupportedException
			d = (Address) super.clone();
			return d;
		}
  //修改Report中的clone函数
	@Override
	public Object clone() throws CloneNotSupportedException {
		Report r = (Report) super.clone();
		r.adress = (Address) adress.clone();//address也克隆一份
		return r;
	}

依然使用上一章节的测试,得到下面的输出。依然将修改的字段高亮,便于比较。

---------------------------unmodified------------------------------
prototype: Report [id=310……, name=Dolphin, temperature=36.2, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------prototype modified 1------------------------------
prototype after prototype modified 1: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after prototype modified 1: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------prototype modified 2------------------------------
prototype after prototype modified 2: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after prototype modified 2: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------clone modified 1------------------------------
prototype after clone modified 1: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after clone modified 1: Report [id=450……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------clone modified 2------------------------------
prototype after clone modified 2: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after clone modified 2: Report [id=450……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=NN, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]

从输出可以看到,不管是原型对象还是克隆得到的对象,经过深拷贝后,修改address这个引用均不会引起另一个对象中该引用的变化。这就是深拷贝的魔力。下面是深拷贝的示意图(用上一张图修修补补,哈哈~)。

在这里插入图片描述

对比浅拷贝而言,深拷贝的好处是不言而喻的,它使得两个对象完全独立。

秉着找茬找到底的态度,再提出一个疑问,深拷贝好是好,但是如果Report类里面不止Address这个类,还有A、B、C、D类,甚至ABCD里面还有EFGH类……的情况下,怎么办?当类的层次变得很深时,深拷贝将会显得捉襟见肘。

3.4 再改进——序列化和反序列化

既然深拷贝也不是很可靠,那么什么最可靠呢?

如题所述,序列化和反序列化,可以写到文件或者内存。建议还是写到内存再从内存读出来,因为这种方式强到爆炸。

  1. Report类实现Serializable接口
public class Report implements Cloneable, Serializable{
//略……
}
  1. Report中组合的类(这里是Address类)也实现Serializable接口,不用再实现Cloneable接口,也就不用再重写Object.clone方法
class Address implements Serializable{
	//略……
	//不用重写Object.clone方法
}
  1. Report类中重写Object.clone()方法,实现序列化和反序列化
@Override
	public Object clone() throws CloneNotSupportedException {
		Object clone = null;
		try (ByteArrayOutputStream out = new ByteArrayOutputStream(); 
				ObjectOutputStream oos = new ObjectOutputStream(out)) {
			oos.writeObject(this);
			byte[] b = out.toByteArray();
			try (ByteArrayInputStream in = new ByteArrayInputStream(b);
					ObjectInputStream ois = new ObjectInputStream(in)) {
				clone = ois.readObject();
				return clone;
			} catch (Exception e) {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return clone;
	}
  1. 测试(测试程序还是那个测试程序,输出内容与深拷贝输出内容也一致,这里就省略了,/狗头))

4.总结

原型模式是创建型设计模式之一,也是比较常用的模式之一。原型模式有浅拷贝和深拷贝之分,浅拷贝在类中组合了其它类时,修改其中之一会影响另外一个;深拷贝有两种实现方式,当类的层次比较浅时可以尝试将组合的类也克隆一份,当类测层次比较深时,使用序列化和反序列化是比较合理的方法。

原型模式用于获取原型对象相同的对象,类似系统中的赋值粘贴。当new一个对象比较繁琐时而你又刚好有了一个对象,原型设计模式就很有用。

除了找一些资料外,也可以参考Java源代码中一些原型模式的写法,如java.util.Date、java.util.HashMap

5. 参考文献

  1. Java源代码
  2. 蜗牛学院 “Java 设计模式”
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值