原型模式在不同语言里的实现

写在前面

前段时间通过阅读《JavaScript高级程序设计》全面学习了一次JavaScript之后,在看到使用原型创建对象时,感觉和Java里的似曾相识又略有不同,于是重新思考了一次原型模式,便有了这篇文章。需要说明的是本文的重点是原型模式本身的思想,尽管我们会讨论Java和JavaScript对原型模式的实现,但真正重要的地方是通过不同语言对原型模式的实现去思考模式本身,而不仅仅是解读语法。以下是今天要讨论的内容:

原型模式思维导图

概念

一句话就可以概括原型模式:以一个对象为样板,复制出另一个新的对象。设计模式是跨语言的,所以我们先抛开严格的语法定义,即先抛开Java里的Cloneable接口,JavaScript里的prototype属性,仅仅着眼于原型模式本身,这里有两个关键词,即“样板”、“复制”,假设我们有一个Person类如下:

public class Person{
	private String name;
	private int age;

	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public int getAge() {
		return age;
	}
}

那么以下这个简单的方法便满足了原型模式的概念。

// 复制一个对象
public Person copyPerson(Person src){
	Person person = new Person(src.getName(), src.getAge());
	return person;
}

// 调用示例
Person p1 = new Person("李四", 20);
Person p2 = copyPerson(p1);
System.out.println(p2.getName() + "--" + p2.getAge());

p2和p1是两个不同的实例(主要是指分别占据不同的内存),但它们归属同一个类,而且属性的内容是一样的,这就是以p1为样板复制一个新对象(p2)。这个例子的写法非常不符合在Java里面使用原型模式的规范,但是它足够简洁地表达了原型模式的根本思想:复制对象。为什么要为了简单而先不谈规范?因为设计模式是跨语言的,我们应当先脱去具体语言的外衣,以最简洁的方式搞懂模式本身,然后才去关注为了实现模式而设计出来的语法。

深拷贝和浅拷贝

拷贝即复制,提到复制对象,就不得不提对于引用类型的深拷贝和浅拷贝,为了加深理解,我们分别列举拷贝对象和拷贝集合的情况。我们为Person加一个字段,现在我们有如下两个对象:

public class Person{
	private String name;
	private int age;
	private Work work; // 工作描述

	public Person(String name, int age, Work work) {
		this.name = name;
		this.age = age;
		this.work = work;
	}

	public String getName() {
		return name;
	}

	public int getAge() {
		return age;
	}

	public Work getWork() {
		return work;
	}
}

public class Work{
	private String name; // 公司名称
	private int salary; // 薪水

	public Work(String name, int salary) {
		this.name = name;
		this.salary = salary;
	}

	public String getName() {
		return name;
	}

	public int getSalary() {
		return salary;
	}
}

拷贝对象

当我们说对某个对象的属性进行拷贝时,深/浅拷贝一般是下面的样子:

// 创建一个待拷贝的对象
Person p = new Person("李四", 20, new Work("abcd有限公司", 10000));

// 浅拷贝
Person p1 = new Person(p.getName(), p.getAge(), p.getWork());

// 深拷贝
Work work = p.getWork();
// 重点在于对Work的拷贝方式有区别
Person p2 = new Person(p.getName(), p.getAge(), new Work(work.getName(), work.getSalary()));

基本数据类型和不可变类型(即String)没有深浅拷贝一说,或者说只有深拷贝,不存在浅拷贝。然而对于引用类型,即Person的Work属性,进行浅拷贝时,新旧Person指向的Work是同一个对象,通过新Person获取并修改Work对象的内容,会影响旧Person。执行深拷贝时,新Person的Work也是一个全新的对象,只是其内容和旧Person的Work一致而已,两者是相互独立的。

拷贝集合

当我们说对集合进行拷贝时,深浅拷贝一般是下面的样子:

// 初始化一个待拷贝的集合
List<Person> srcList = new ArrayList<>();
for(int i = 0; i < 10; i++){
	srcList.add(new Person("李四" + i, i, new Work(i + "有限公司", 10000 * i)));
}

// 浅拷贝
List<Person> newList = new ArrayList<>();
for(int i = 0, size = srcList.size(); i < size; i++){
	newList.add(srcList.get(i));
}

// 深拷贝
List<Person> newList = new ArrayList<>();
Person person;
Work work;
for(int i = 0, size = srcList.size(); i < size; i++){
	person = srcList.get(i);
	work = person.getWork();
	// 这里需要注意的是,不仅仅要创建新的Person,每个Person的Work也是新创建的
	newList.add(new Person(person.getName(), person.getAge(), new Work(work.getName(), work.getSalary())));
}

对于浅拷贝,两个集合所指向的都是内存里的同一套对象,通过其中一个集合获取并修改对象,会影响到另外一个集合。对于深拷贝,内存里实际上有两套相互独立的对象,通过其中一个集合获取并修改对象,对另一个集合没有任何影响。

至此,我们知道了原型模式的关键在于提供“样板”以及对样板进行“复制”,也了解了深浅拷贝,现在我们从抽象的设计模式落实到具体的语法实现。

原型模式在Java里的实现

Java通过Cloneable接口提供样板,当一个类实现了Cloneable接口,表示它的对象可以被作为样板进行复制。值得一提的是Cloneable是个空接口:

public interface Cloneable {
}

它里面没有任何方法,它仅仅表示这个类的对象可以被复制。原型模式的另一个关键“复制”是由定义在Object类里面的clone()方法来表示的,你需要在你的自定义类里面重写clone()方法,并调用Object类的clone()方法来进行复制。

为什么会搞得那么奇怪呢?因为Java提供了真正“复制”对象的方法,不同于通过new来创建新对象,它可以直接在内存层面复制对象,而Java又不允许程序员直接操作内存,于是只能使用Object的clone()来复制对象。实际上,你甚至都看不到JDK里面clone()的源码:

protected native Object clone() throws CloneNotSupportedException;

因为它不是Java的方法,而是native的方法。

这同时解释了为什么clone()没有定义在Cloneable接口里面(因为程序员没法操作内存,不能实现真正能复制对象的clone()方法),以及为什么要在重写clone()的时候调用Object类的clone()方法(因为这个方法才能真正在内存层面直接复制对象)。至于为什么要实现Cloneable接口,因为clone()方法会检查对象是否实现了Cloneable接口,如果没有实现,会抛出CloneNotSupportedException异常。

最后需要注意的是,Object的clone()方法对引用类型执行的是浅拷贝。

于是使用Cloneable来实现原型模式的写法如下:

public class Person implements Cloneable{
	private String name;
	private int age;
	private Work work;

	public Person(String name, int age, Work work) {
		this.name = name;
		this.age = age;
		this.work = work;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public Work getWork() {
		return work;
	}

	public void setWork(Work work) {
		this.work = work;
	}
    
	@Override
	protected Person clone() {
		Person person = null;
		try{
			person = (Person)super.clone();
		} catch (CloneNotSupportedException e){
			e.printStackTrace();
		}
		// 注意对引用类型对象进行深拷贝,这样才能得到两个完全独立的Person,
		// 当然如果你就是不想进行深拷贝,也可以不要这行代码
		Work work = person.getWork();
		person.setWork(new Work(work.getName(), work.getSalary()));
		return person;
	}
}

使用:
Person p1 = new Person("李四", 20, new Work("abcd有限公司", 10000));
Person p2 = p1.clone();

小结一下Java是如何实现原型模式的几个关键点的:

  • 提供“样板”:Java通过Cloneable接口表示一个类的对象可以被作为样板进行复制。
  • 复制样板:在Object类里面提供clone()方法实现对象在内存层面的复制。
  • 深浅拷贝:Object的clone()方法对引用类型执行的是浅拷贝,程序员在重写clone()时可以在调用Object的clone()获取到新对象后“手动地”对引用类型进行深拷贝。

原型模式在JavaScript里的实现

和Java相比一个很大的区别在于,JavaScript没有“类”的概念,JavaScript通过函数(为了区分普通函数,下文用构造函数来称呼)创建对象,或者说是通过“new + 构造函数”来创建对象。

在Java里面,可以通过至少两类方式来创建对象:使用new通过类来创建对象、通过原型模式以clone()的方式创建对象。然而在JavaScript里面,由于没有类的概念,并且函数本身也是一个对象,所以通过“new + 构造函数”来创建对象,本身就是以一个对象为样板复制出一个新对象。这里的样板就是构造函数,而“new + 构造函数名”返回的对象,便是被复制出来的对象。可以说,在JavaScript里面创建对象“天然”地符合原型模式的思想。

现在我们已经知道了在JavaScript里面,通过定义构造函数来提供样板,使用“new + 构造函数”即执行了复制对象的操作,实际上整个复制过程都没有让程序员干预,那么问题来了,深浅拷贝怎么控制呢?答案是通过系统为每一个构造函数自动添加的prototype属性来控制。我们通过一张图片来看看构造函数、原型、创建出来的对象的关系:

JS里的原型模式

系统会为你定义的每一个构造函数添加一个prototype属性,这个属性指向另外一个系统为你自动创建的对象,这个对象被称为“原型”。你可以通过构造函数来访问这个对象,为它添加属性或方法。在复制对象的时候(即使用“new + 构造函数”创建对象的时候),所有构造函数里定义的东西(包括属性和方法)会进行深拷贝 ,所有原型里的东西(包括属性和方法)会进行浅拷贝,这里所谓的浅拷贝,就是每个被创建出来的对象里面有一个属性指向它们共同的原型——即构造函数的原型。

小结一下JavaScript是如何实现原型模式的几个关键点的:

  • 提供“样板”:每一个函数都可以被当作一个样板。
  • 复制样板:使用“new + 构造函数”即可进行复制。
  • 深浅拷贝:在构造函数里定义的东西会被深拷贝,在原型里定义的东西会被浅拷贝。

总结:重要的是思想,不是语法

交替思考Java和JavaScript对原型模式的实现,其共同点在于它们都实现了原型模式的关键:提供样板和复制对象,也都让程序员可以自由地配置深浅拷贝,只不过基于各自的语法以不同的方式实现。

其实原型模式非常简单,就是提供样板和复制对象(以及深浅拷贝)两件事,甚至在Java里面你只要知道Cloneable接口以及如何重写clone()方法即可。然而学习设计模式重要的不在于语法,而在于理解模式本身的思想和行为,笔者认为一个Java程序员去理解JavaScript的原型很有意思,因为JavaScript没有类,没有接口,基于一个完全不同的语法环境去思考如何实现一个你曾经学过的设计模式能够检验你是否真正学会了这个模式,而不是仅仅记住了Cloneable和clone()。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章