首页 > 编程笔记 > Java笔记 阅读:14

Java原型模式详解(附带实例)

原型模式是一个较为常见的设计模式,也属于经典的 GoF 设计模式。

原型模式用来免除复杂的实例化过程,让用户不用总是重复这个过程,也让设计者无须提供过多的工厂来制作各种实例,而是可以让用户在现有的实例上克隆并加以调整。

如果你不想总是重复某一个复杂的实例化过程,或者不想仅仅为了制作配置略有区别的产品而设计过多的工厂,那么原型模式会很有帮助。原型模式能够把已有的某个对象当作原型,并根据该对象克隆新的实例。克隆出来的实例与当作原型的那个实例之间是相互独立的,可以各自进行定制。

这样的话,我们就可以把创建实例的逻辑封装起来,使得用户不需要手工执行这个实例化流程,而且用户也无法干预这个流程,他们只需要在现有的实例上克隆就行了。

原型模式在JDK中的运用

JDK 里有好多地方都出现了原型模式。Java 集合框架中的一些类型实现了 Cloneable 接口,于是就继承了该接口所定义的 clone 方法。你可以在这些类型的实例上调用该方法来克隆出新的实例。

例如,你可以通过在某个 ArrayList 上调用 clone() 方法来创建它的浅拷贝,拷贝出来的这个 ArrayList 的每个元素的值均与原来的 ArrayList 相同,但你可以修改后者的内容,而不影响前者相应位置上的元素值。

还有一个出现原型模式的地方是 Calendar 类,它位于 java.base 模块的 java.util 包中。Calendar 类在实现其中的某些方法时,也利用该模式来简化实现代码,这样它就不用在现有对象上进行修改,而是可以在克隆出来的对象上进行运算。

例如,这个类在实现它的 getActualMinimum() 与 getActualMaximum() 方法时就是这么做的。

Java原型模式范例

在研发某一款产品的过程中,我们可能会从少数几个产品出发,不断调整这些产品的内部属性,从而令产品达到较好的状态。在这种情况下,没必要针对每一种属性组合都设计一个工厂类或建造者类,那样会让类的数量变得比较多。

还是以车辆为例,例如,我们正处在早期研发阶段,所以想反复调整每一种车型的配置,于是,我们需要方便地获取该车型的实例,以便对实例的各个属性做出调整,但同时我们又要求调整之后的实例应该与调整之前的实例具有不同的身份,以便追踪管理。

假如我们是在同一个实例上做调整的,就无法分别管理调整之后与调整之前的车辆。为了应对这一问题,可以考虑用原型模式来克隆新的实例,如下图所示:


图 1 用原型模式克隆新的实例

【实例】原型模式让用户能够从现有的实例中克隆新的实例。
public static void main(String[] args) {
    Vehicle fastCar1 = VehicleCache.getVehicle("fast-car");
    Vehicle fastCar2 = VehicleCache.getVehicle("fast-car");
    fastCar1.move();
    fastCar2.move();
    System.out.println("equals : " + (fastCar1.equals(fastCar2)));
}
程序输出结果如下:

Pattern Prototype: vehicle prototype 1
fast car, move
fast car, move
equals : false
fastCar1:FastCar@659e0bfd
fastCar2:FastCar@2a139a55

运用原型模式很容易就可以把某辆车当成原型,克隆出一辆各个属性与之完全相同的车。

原型模式令用户能够根据需要来重制(也就是克隆)现有的实例。再看一个实例,我们设计一个抽象类作为每一种具体产品的基础,而且让这个类实现 Cloneable 接口并覆写 Object 类的 clone() 方法,我们会在这个方法里写出详细的克隆过程,如果子类的克隆过程没有需要调整的地方,那么直接沿用本类的 clone() 方法即可。

【实例】作为各产品基类的 Vehicle 抽象类必须实现 Cloneable 接口并覆写 clone() 方法,这样用户才能通过调用 clone() 方法来克隆 Vehicle 实例。
abstract class Vehicle implements Cloneable {
    protected final String type;

    Vehicle(String t) {
        this.type = t;
    }

    abstract void move();

    @Override
    protected Object clone() {
        Object clone = null;
        try {
            clone = super.clone();
        } catch (CloneNotSupportedException e) {
            // Handle exception
        }
        return clone;
    }
}

每一种具体的车辆实现都需要扩展父类 Vehicle:
class SlowCar extends Vehicle {
    SlowCar() {
        super("slow car");
    }
    @Override
    void move() {...}
}
在我们设计的这个原型模式方案中,作为原型的这些 Vehicle 型实例都放置在一个内部的缓存区(也就是 map)里。这样的话,我们可以提供一个静态方法,让用户先通过这个方法从缓存中获取某个原型,然后再进行克隆。

由于缓存区与静态方法所在的 VehicleCache 类是个纯粹的工具类,没有实例级别的状态,因此我们将它的构造器设置为 private,以防止其他代码创建该类的实例。

【实例】设计一个工具类,并在其中开设内部缓存区,将一些预先制备好的原型放在里面,同时提供一个静态方法,让用户获取这些原型。
final class VehicleCache {
    private static final Map<String, Vehicle> map =
        Map.of("fast-car", new FastCar(), "slow-car", new SlowCar());

    private VehicleCache() {}

    static Vehicle getVehicle(String type) {
        Vehicle vehicle = map.get(type);
        if (vehicle == null) throw
            new IllegalArgumentException("not allowed:" + type);
        return (Vehicle) vehicle.clone();
    }
}
通过这个例子,大家看到,用户能够从现有的某个原型出发获取该原型的一个复制品。然后,用户可以根据需求在这个复制品上做出调整,由于克隆出来的复制品与作为基板的原型是身份不同的实例,所以这些调整不会影响原型本身。

总结

原型模式适合用来实现动态加载,也适合用来降低代码的复杂度。

假如不使用这个模式,那么你可能要实现多个工厂子类,以创建不同类型的实例或创建配置有所区别的同类型实例,这样会导致代码里出现一些不必要的抽象。

当然,原型模式本身也要实现接口(即 Cloneable 接口),但这样做让我们可以把克隆的详细过程封装在 clone() 方法里,而不将这个过程公布给用户。他们只需要调用 clone() 方法来克隆新的实例,不需要自己手动执行复杂的实例化过程。

使用原型模式意味着用户不应该而且也不能够干预这个复杂的实例创建过程。如果你面对的是一套遗留代码,那么创建实例的过程可能会频繁变动。若能适当运用该模式,则可将这些变动局限在一定范围内,即便你对实例化过程做出多次调整,也不会让其他代码受到太大影响。

相关文章