原型模式
原型模式 (Prototype Pattern) 是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,属于创建型模式。
原型模式的核心在于拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能有较大提升。
原型模式主要包含三个角色:
- 客户 (Client):客户类提出创建对象的请求。
- 抽象原型 (Prototype):规定拷贝接口。
- 具体原型 (Concrete Prototype):被拷贝的对象。
对不通过 new 关键字,而是通过对象拷贝来实现创建对象的模式就称作原型模式。
原型模式主要适用于以下场景:
- 类初始化消耗资源较多。
- 每次新创建一个对象需要非常繁琐的过程(数据准备、访问权限等)
- 构造函数比较复杂。
- 循环体中生产大量对象时。
案例
举个例子,我们有一个 Student 对象:
public class Student {
private int grade;
private int classroom;
private String name;
private int age;
private String major;
}
当我们要给计算机专业三年级二班添加以为新同学时,可能会有这样的代码:
Student stu = queryObject("SELECT * FROM tb_student WHERE grade = 3 AND classroom = 2 AND major = 'CS' LIMIT 1");
Student newStu = new Student();
newStu.setGrade(stu.getGrade());
newStu.setClassroom(stu.getClassroom());
newStu.setName("Tom");
newStu.setAge(20);
newStu.setMajor(stu.getMajor());
如果 Student 这个类有非常多的属性,这样的方式就太繁琐了。我们可能会想到利用反射实现一个稍微偷懒的方式:
public class BeanUtils {
public static Object copy(Object protorype) {
Class clazz = protorype.getClass();
Object returnValue = null;
try {
returnValue = clazz.getConstructor().newInstance();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
field.set(returnValue, field.get(protorype));
}
} catch(Exception e) {
e.printStackTrace();
}
return returnValue;
}
}
虽然看起来优雅了些,但基于反射的实现还是存在着必须属性命名严格一致,性能低下的问题,并且实现原理上也和手动 get,set 没有本质区别。
浅克隆
这时,我们的目光转向了 JDK 自带的一个接口:Cloneable
。
首先来看我们的原型对象:
public class StudentPrototype implements Cloneable {
private int age;
private String name;
private List<String> courses;
@Override
public StudentPrototype clone() {
try {
return (StudentPrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
然后测试一下通过浅克隆来新建一个 Student:
public class ShallowCloneTest {
public static void main(String[] args) {
StudentPrototype studentPrototype = new StudentPrototype();
// prepare prototype
studentPrototype.setAge(18);
studentPrototype.setName("Tom");
List<String> courses = new ArrayList<String>();
courses.add("Java course");
courses.add("Python course");
studentPrototype.setCourses(courses);
// shallow clone
StudentPrototype student1 = studentPrototype.clone();
System.out.println("Prototype: " + studentPrototype.toString());
System.out.println("Cloned: " + student1.toString());
}
}
看一下输出结果,看起来好像没什么问题:
Prototype: StudentPrototype{age=18, name='Tom', courses=[Java course, Python course]}
Cloned: StudentPrototype{age=18, name='Tom', courses=[Java course, Python course]}
但是我们稍作深入,浅克隆的问题就暴露了:
// alter prototype attributes
student1.getCourses().add("AI course");
System.out.println("Prototype: " + studentPrototype.toString());
System.out.println("Cloned: " + student1.toString());
输出结果:
Prototype: StudentPrototype{age=18, name='Tom', courses=[Java course, Python course, AI course]}
Cloned: StudentPrototype{age=18, name='Tom', courses=[Java course, Python course, AI course]}
我们明明修改的是克隆出的新对象的 Courses 属性,结果连原型对象也一并被修改了。这显然不符合我们的预期。因为我们希望克隆出来的对象应该和原型对象是两个独立的对象,不应该再有联系了。
从测试结果来看,浅克隆正如其名,对于基本数据类型,他完美的完成了拷贝的任务,但是对于列表类型,则只是拷贝了一份引用,指向的却仍然是同一个内存地址。因此我们修改任意一个对象中的属性值,prototype 和 cloneType 的 courses 值都会改变。
有同学可能会说,这个问题好解决啊,既然浅克隆默认只复制值对象,那我们自己把引用对象拷贝一份不就可以了么:
public class StudentPrototype implements Cloneable {
private int age;
private String name;
private List<String> courses;
@Override
public StudentPrototype clone() {
try {
StudentPrototype newStudent = (StudentPrototype)super.clone();
List<String> newCourses = newStudent.getCourses().clone();
newStudent.setCourses(newCourses);
return newStudent;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
这样做确实能解决我们现有的问题,可是如果还有 List<List<CustomObject>>
这样的类型呢?还要去嵌套实现 List 中的 List,甚至 CustomObject 中所有属性的克隆吗?显然这并不是一个通用的解决方法。
深克隆
为了解决浅克隆存在的问题,我们继续来看深克隆。
深克隆的三种方式:
- 手动赋值(用反射手动赋值);
- 序列化与反序列化,要求拷贝的对象实现了 Serializable 。然而有些限制,比如 Map 类不支持,使用 HashMap 即可;
- 利用 JSON 对象克隆,本质上是反射。
来看一下分别基于序列化和基于 JSON 的深克隆方式:
public interface ICloneablePrototype<T> {
T deepCloneJSON();
T deepCloneSerializable();
}
public class StudentPrototype implements ICloneablePrototype<StudentPrototype>, Serializable {
public StudentPrototype deepCloneSerializable() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(bos);
outputStream.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (StudentPrototype) ois.readObject();
} catch (Exception e){
e.printStackTrace();
return null;
}
}
public StudentPrototype deepCloneJSON() {
Gson gson = new Gson();
String json = gson.toJson(this);
return gson.fromJson(json, this.getClass());
}
}
克隆破坏单例模式
如果我们克隆的目标的对象是单例对象,那就意味着,克隆方法会可以绕过构造方法创建一个新对象,从而破坏了单例。实际上防止克隆破坏单例的解决思路非常简单:
- 单例不实现 Cloneable 接口
- 重写
clone()
方法直接返回单例对象即可
@Override
protected Object clone() throws CloneNotSupportedException {
return INSTANCE;
}
JDK 中的原型模式
先看 JDK 中的 Cloneable 接口,只有一个标记定义:
public interface Cloneable {
}
继续找找看哪些接口实现了 Cloneable 接口,例如 ArrayList
类:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
public Object clone() {
try {
ArrayList<?> v = (ArrayList)super.clone();
v.elementData = Arrays.copyOf(this.elementData, this.size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException var2) {
throw new InternalError(var2);
}
}
}
可以看到,ArrayList 类的 clone 方法中只是将 List 中的元素遍历拷贝了一遍。那么我们再思考一下,这样的形式真的实现了数组的深克隆吗?用代码简单验证一下就知道了:
class Element {
public int a;
}
public class DeepCloneTest {
public static void main(String[] args) {
ArrayList<Element> list1 = new ArrayList<Element>();
Element ele = new Element();
list1.add(ele);
ArrayList<Element> list2 = (ArrayList<Element>)list1.clone();
System.out.println(list1 == list2); // false
System.out.println(list1.get(0) == list2.get(0)); // true
}
}
真相只有一个:ArrayList 的 clone 方法也是浅克隆,内部的引用对象还是同一份。
一般的,实现了 Cloneable 接口的对象的 clone 方法都是浅克隆。
原型模式的优缺点
优点:
- 性能优良,JDK 自带的原型模式基于内存的二进制流的拷贝,和直接 new 一个对象相比性能更高;
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
缺点:
- 需要为每一个类配置一个克隆方法;
- 克隆方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违反了开闭原则; - 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,实现深克隆比较麻烦。
建造者模式
建造者模式 (Builder Pattern) 是将一个复杂对象的构建过程与它的表示分离,使得同样的构建过程可以创建不同的表示,属于创建型模式。
使用建造者模式,只需指定需要建造的类型,就可以获得对象而不需要了解对象的建造过程及细节。
建造者模式适用于创建对象需要很多步骤,但是步骤的顺序不一定固定。如果一个对象有非常复杂的内部结构(很多属性),可以将复杂对象的创建和使用(属性设置)的操作分离。
建造者模式的设计中,主要有四个角色:
- 产品 (Product):要创建的产品类对象。
- 建造者抽象 (Builder):建造者的抽象类,规范产品对象的各个组成部分的建造,一般由子类实现具体的建造过程。
- 建造者 (ConcreteBuilder):具体的 Builder 类,根据不同的业务逻辑,具体化对象的各个组成部分的创建。
- 调用者 (Director):调用具体的建造者,来创建对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
应用场景
建造者模式适用于一个具有较多的零件的复杂产品的创建过程。由于需求的变化,组成这个复杂产品的各个零件经常有着很大的变化,但是它们的组合方式却相对稳定。
建造者模式适用于以下几种场景:
- 相同的方法,不同的执行顺序会产生不同的结果时。
- 多个部件或零件,都可以装配到一个对象中,但是产生的结果又不相同。
- 产品类非常复杂,或者产品类中的调用顺序不同产生不同的作用。
- 当初始化一个对象特别复杂,参数多,而且很多参数都具有默认值时。
大部分建造者模式的对象还支持链式写法,说到这里相信大部分人都懂了:
StringBuilder sb = new StringBuilder("StringBuilder ")
.append("hello").append(" ")
.append("world").append("!");
System.out.println(sb.toString());
而这个链式写法的神奇之处,其实就在于其特殊的返回值:
public final class StringBuilder extends AbstractStringBuilder implements Serializable, Comparable<StringBuilder>, CharSequence {
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
MyBatis 中的建造者模式
MyBatis 中的 CacheBuilder
类,通过构建各种装饰然后最后再构建对象:
public class CacheBuilder {
private final List<Class<? extends Cache>> decorators;
public CacheBuilder addDecorator(Class<? extends Cache> decorator) {
if (decorator != null) {
this.decorators.add(decorator);
}
return this;
}
public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
}
再来看另一个类 SqlSessionFactoryBuilder
:
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment) {
return build(reader, environment, null);
}
public SqlSessionFactory build(Reader reader, Properties properties) {
return build(reader, null, properties);
}
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
}
Spring 中的建造者模式
在 Spring 中自然也少不了建造者模式的身影,比如 BeanDefinitionBuilder
类,通过调用 getBeanDefinition()
方法构建 BeanDefinition
对象:
public final class BeanDefinitionBuilder {
public AbstractBeanDefinition getBeanDefinition() {
this.beanDefinition.validate();
return this.beanDefinition;
}
}
其中决定可以构建的类型的方法有 genericBeanDefinition
,childBeanDefinition
,rootBeanDefinition
等等,可装配的属性有 setFactoryMethod
,setInitMethodName
,setScope
等等,然而不管是什么类型,装配怎样的属性,其最终构建的一定是 AbstractBeanDefinition
这个类的对象。
因此当我们需要创建一个 BeanDefinition
的时候,不需要在创建时就指出需要创建的类型,创建完成后可以按照我们需要进行装配,最后就可以构建出我们想要的对象。
建造者模式小结
建造者模式的优点:
- 封装性好,创建和使用分离;
- 扩展性好,建造类之间独立,一定程度上解耦。
建造者模式的缺点:
- 需要借助额外的 Builder 对象;
- 产品内部的结构发生变化,也需要修改建造者,改动成本较大。
建造者模式和工厂模式的区别
- 建造者模式更加注重方法的构建组成,工厂模式注重于创建对象。
- 创建对象的粒度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的是标准对象。
- 关注重点不一样,工厂模式模式只需要把对象创建出来就可以了,而建造者模式中不仅要创建出这个对象,还要知道这个对象由哪些部件组成。
- 建造者模式根据建造过程中的顺序不一样,最终的对象部件组成也不一样。
文章评论