创建型模式-原型模式与建造者模式

2020-03-01 1839点热度 0人点赞 0条评论

原型模式

原型模式 (Prototype Pattern) 是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,属于创建型模式。

原型模式的核心在于拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能有较大提升。

原型模式主要包含三个角色:

  • 客户 (Client):客户类提出创建对象的请求。
  • 抽象原型 (Prototype):规定拷贝接口。
  • 具体原型 (Concrete Prototype):被拷贝的对象。
对不通过 new 关键字,而是通过对象拷贝来实现创建对象的模式就称作原型模式。

原型模式主要适用于以下场景:

  1. 类初始化消耗资源较多。
  2. 每次新创建一个对象需要非常繁琐的过程(数据准备、访问权限等)
  3. 构造函数比较复杂。
  4. 循环体中生产大量对象时。

案例

举个例子,我们有一个 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 中所有属性的克隆吗?显然这并不是一个通用的解决方法。

深克隆

为了解决浅克隆存在的问题,我们继续来看深克隆。

深克隆的三种方式:

  1. 手动赋值(用反射手动赋值);
  2. 序列化与反序列化,要求拷贝的对象实现了 Serializable 。然而有些限制,比如 Map 类不支持,使用 HashMap 即可;
  3. 利用 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());
    }
}

克隆破坏单例模式

如果我们克隆的目标的对象是单例对象,那就意味着,克隆方法会可以绕过构造方法创建一个新对象,从而破坏了单例。实际上防止克隆破坏单例的解决思路非常简单:

  1. 单例不实现 Cloneable 接口
  2. 重写 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) 是将一个复杂对象的构建过程与它的表示分离,使得同样的构建过程可以创建不同的表示,属于创建型模式。

使用建造者模式,只需指定需要建造的类型,就可以获得对象而不需要了解对象的建造过程及细节。

建造者模式适用于创建对象需要很多步骤,但是步骤的顺序不一定固定。如果一个对象有非常复杂的内部结构(很多属性),可以将复杂对象的创建和使用(属性设置)的操作分离。

建造者模式的设计中,主要有四个角色:

  1. 产品 (Product):要创建的产品类对象。
  2. 建造者抽象 (Builder):建造者的抽象类,规范产品对象的各个组成部分的建造,一般由子类实现具体的建造过程。
  3. 建造者 (ConcreteBuilder):具体的 Builder 类,根据不同的业务逻辑,具体化对象的各个组成部分的创建。
  4. 调用者 (Director):调用具体的建造者,来创建对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。

应用场景

建造者模式适用于一个具有较多的零件的复杂产品的创建过程。由于需求的变化,组成这个复杂产品的各个零件经常有着很大的变化,但是它们的组合方式却相对稳定。

建造者模式适用于以下几种场景:

  1. 相同的方法,不同的执行顺序会产生不同的结果时。
  2. 多个部件或零件,都可以装配到一个对象中,但是产生的结果又不相同。
  3. 产品类非常复杂,或者产品类中的调用顺序不同产生不同的作用。
  4. 当初始化一个对象特别复杂,参数多,而且很多参数都具有默认值时。

大部分建造者模式的对象还支持链式写法,说到这里相信大部分人都懂了:

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;
    }
}

其中决定可以构建的类型的方法有 genericBeanDefinitionchildBeanDefinitionrootBeanDefinition 等等,可装配的属性有 setFactoryMethodsetInitMethodNamesetScope 等等,然而不管是什么类型,装配怎样的属性,其最终构建的一定是 AbstractBeanDefinition 这个类的对象。

因此当我们需要创建一个 BeanDefinition 的时候,不需要在创建时就指出需要创建的类型,创建完成后可以按照我们需要进行装配,最后就可以构建出我们想要的对象。

建造者模式小结

建造者模式的优点:

  • 封装性好,创建和使用分离;
  • 扩展性好,建造类之间独立,一定程度上解耦。

建造者模式的缺点:

  • 需要借助额外的 Builder 对象;
  • 产品内部的结构发生变化,也需要修改建造者,改动成本较大。

建造者模式和工厂模式的区别

  1. 建造者模式更加注重方法的构建组成,工厂模式注重于创建对象。
  2. 创建对象的粒度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的是标准对象。
  3. 关注重点不一样,工厂模式模式只需要把对象创建出来就可以了,而建造者模式中不仅要创建出这个对象,还要知道这个对象由哪些部件组成。
  4. 建造者模式根据建造过程中的顺序不一样,最终的对象部件组成也不一样。

SilverLining

也可能是只程序猿

文章评论