创建型-原型模式(Prototype)

3/17/2022 设计模式

摘要

JDK:1.8.0_202

# 一:场景问题

# 1.1 订单处理系统

现在有一个订单处理的系统,里面有个保存订单的业务功能,在这个业务功能里面,客户有这么一个需求:每当订单的预定产品数量超过1000的时候,就需要把订单拆成两份订单来保存,如果拆成两份订单后,还是超过1000,那就继续拆分,直到每份订单的预定产品数量不超过1000。至于为什么要拆分,原因是好进行订单的后续处理,后续是由人工来处理,每个人工工作小组的处理能力上限是1000。

根据业务,目前的订单类型被分成两种:一种是个人订单,一种是公司订单。现在想要实现一个通用的订单处理系统,也就是说,不管具体是什么类型的订单,都要能够正常的处理。

该怎么实现呢?

# 1.2 不用模式的解决方案

订单接口:(OrderApi.java)

/**
 * 订单的接口
 */
public interface OrderApi {

    /**
     * 获取订单产品数量
     *
     * @return 订单中产品数量
     */
    int getOrderProductNum();

    /**
     * 设置订单产品数量
     *
     * @param num 订单产品数量
     */
    void setOrderProductNum(int num);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

个人订单实现:(PersonalOrder.java)

/**
 * 个人订单对象
 */
public class PersonalOrder implements OrderApi {
    /**
     * 订购人员姓名
     */
    private String customerName;
    /**
     * 产品编号
     */
    private String productId;
    /**
     * 订单产品数量
     */
    private int orderProductNum = 0;

    @Override
    public int getOrderProductNum() {
        return this.orderProductNum;
    }

    @Override
    public void setOrderProductNum(int num) {
        this.orderProductNum = num;
    }

    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }

    public String getProductId() {
        return productId;
    }

    public void setProductId(String productId) {
        this.productId = productId;
    }

    public String toString() {
        return "本个人订单的订购人是=" + this.customerName + ",订购产品是=" + this.productId + ",订购数量为=" + this.orderProductNum;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

企业订单实现:(EnterpriseOrder.java)

/**
 * 企业订单对象
 */
public class EnterpriseOrder implements OrderApi {
    /**
     * 企业名称
     */
    private String enterpriseName;
    /**
     * 产品编号
     */
    private String productId;
    /**
     * 订单产品数量
     */
    private int orderProductNum = 0;

    @Override
    public int getOrderProductNum() {
        return this.orderProductNum;
    }

    @Override
    public void setOrderProductNum(int num) {
        this.orderProductNum = num;
    }

    public String getEnterpriseName() {
        return enterpriseName;
    }

    public void setEnterpriseName(String enterpriseName) {
        this.enterpriseName = enterpriseName;
    }

    public String getProductId() {
        return productId;
    }

    public void setProductId(String productId) {
        this.productId = productId;
    }

    public String toString() {
        return "本企业订单的订购企业是=" + this.enterpriseName + ",订购产品是=" + this.productId + ",订购数量为=" + this.orderProductNum;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

上面不把相同数据,抽出一个父类,主要是,这里仅是示意和为了后续示例的重点突出

通用订单处理:(OrderBusiness.java)

/**
 * 处理订单的业务对象
 */
public class OrderBusiness {

    /**
     * 创建订单的方法
     *
     * @param order 订单的接口对象
     */
    public void saveOrder(OrderApi order) {
        // 根据业务要求,当订单预定产品数量超过1000时,就要把订单拆成两份订单

        // 1. 判断当前的预定产品数量是否大于1000
        while (order.getOrderProductNum() > 1000) {
            // 2. 如果大于1000,还需要继续拆分
            // 2.1 再新建一份订单,跟传入的订单除了数量不一样外,其他都相同
            OrderApi newOrder = null;
            if (order instanceof PersonalOrder) {
                // 创建相应的新的订单对象
                PersonalOrder p2 = new PersonalOrder();
                // 然后进行赋值,但是产品数量为1000
                PersonalOrder p1 = (PersonalOrder) order;
                p2.setCustomerName(p1.getCustomerName());
                p2.setProductId(p1.getProductId());
                p2.setOrderProductNum(1000);
                // 然后再设置给newOrder
                newOrder = p2;
            } else if (order instanceof EnterpriseOrder) {
                // 创建相应的订单对象
                EnterpriseOrder e2 = new EnterpriseOrder();
                // 然后进行赋值,但是产品数量为1000
                EnterpriseOrder e1 = (EnterpriseOrder) order;
                e2.setEnterpriseName(e1.getEnterpriseName());
                e2.setProductId(e1.getProductId());
                e2.setOrderProductNum(1000);
                // 然后再设置给newOrder
                newOrder = e2;
            }

            // 2.2 原来的订单保留,把数量设置成减少1000
            order.setOrderProductNum(order.getOrderProductNum() - 1000);

            // 然后是业务功能处理,省略,直接打印输出
            System.out.println("拆分生成订单==" + newOrder);
        }
        // 3. 不超过1000,那就直接业务功能处理,省略,直接打印输出
        System.out.println("订单==" + order);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

客户端:(OrderClient.java)

public class OrderClient {

    public static void main(String[] args) {
        // 创建订单对象,这里为了演示简单,直接new了
        PersonalOrder op = new PersonalOrder();
        // 设置订单数据
        op.setOrderProductNum(2925);
        op.setCustomerName("张三");
        op.setProductId("P0001");

        // 这里获取业务处理的类,也直接new了,为了简单,连业务接口都不能做
        OrderBusiness ob = new OrderBusiness();
        // 调用业务来保存订单对象
        ob.saveOrder(op);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行结果:

运行结果

# 1.3 有何问题

看起来,上面的实现确实不难,好像也能够通用的进行订单处理,而不需要关心订单的类型和具体实现这样的功能。

仔细想想,真的没有关心订单的类型和具体实现吗?答案是 "否定的"

事实上,在实现订单处理的时候,上面的实现是按照订单的类型和具体实现来处理的,就是instanceof的那一段。有朋友可能会问,这样实现有何不可吗?这样的实现有如下几个问题:

既然想要实现通用的订单处理,那么对于订单处理的实现对象,是不应该知道订单的具体实现的,更不应该依赖订单的具体实现。但是上面的实现中,很明显订单处理的对象依赖了订单的具体实现对象。

这种实现方式另外一个问题就是:难以扩展新的订单类型。假如现在要加入一个大客户专用订单的类型,那么就需要修改订单处理的对象,要在里面添加对新的订单类型的支持,这算哪门子的通用处理。

因此,上面的实现是不太好的,把上面的问题再抽象描述一下:已经有了某个对象实例后,如何能够快速简单地创建出更多的这种对象?比如上面的问题,就是已经有了订单接口类型的对象实例,然后在方法中需要创建出更多的这种对象。怎么解决呢?

# 二:解决方案

用来解决上述问题的一个合理的解决方案就是原型模式。

原型模型:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。

# 2.1 解决思路

仔细分析上面的问题,在saveOrder方法里面,已经有了订单接口类型的对象实例,是从外部传入的,但是这里只是知道这个实例对象的种类是订单的接口类型,并不知道其具体的实现类型,也就是不知道它到底是个人订单还是企业订单,但是现在需要在这个方法里面创建一个这样的订单对象,看起来就像是要通过接口来创建对象一样。

原型模式就可以解决这样的问题,原型模式会要求对象实现一个可以 "克隆" 自身的接口,这样就可以通过拷贝或者是克隆一个实例对象本身,来创建一个新的实例。如果把这个方法定义在接口上,看起来就像是通过接口来创建了新的接口对象。

这样一来,通过原型实例创建新的对象,就不再需要关心这个实例本身的类型,也不关心它的具体实现,只要它实现了克隆自身的方法,就可以通过这个方法来获取新的对象,而无须再去通过new来创建。

# 2.2 模式结构和说明

模型结构

  • Prototype:声明一个克隆自身的接口,用来约束想要克隆自己的类,要求它们都要实现这里定义的克隆方法。
  • ConcretePrototype:实现Prototype接口的类,这些类真正实现了克隆自身的功能。
  • Client:使用原型的客户端,首先要获取到原型实例对象,然后通过原型实例克隆自身来创建新的对象实例。

# 2.3 示例代码

原型接口的定义:(OrderClient.java)

public class OrderClient {

    public static void main(String[] args) {
        // 创建订单对象,这里为了演示简单,直接new了
        PersonalOrder op = new PersonalOrder();
        // 设置订单数据
        op.setOrderProductNum(2925);
        op.setCustomerName("张三");
        op.setProductId("P0001");

        // 这里获取业务处理的类,也直接new了,为了简单,连业务接口都不能做
        OrderBusiness ob = new OrderBusiness();
        // 调用业务来保存订单对象
        ob.saveOrder(op);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

原型实现对象:(ConcretePrototype1.java 和 ConcretePrototype2.java)

/**
 * 克隆的具体实现对象
 */
public class ConcretePrototype1 implements Prototype {
    @Override
    public Prototype clone() {
        //最简单的克隆,新建一个自身对象,由于没有属性,就不去复制值了
        Prototype prototype = new ConcretePrototype1();
        return prototype;
    }
}
1
2
3
4
5
6
7
8
9
10
11
/**
 * 克隆的具体实现对象
 */
public class ConcretePrototype2 implements Prototype {
    @Override
    public Prototype clone() {
        //最简单的克隆,新建一个自身对象,由于没有属性,就不去复制值了
        Prototype prototype = new ConcretePrototype2();
        return prototype;
    }
}
1
2
3
4
5
6
7
8
9
10
11

客户端:(Client.java)

/**
 * 使用原型的客户端
 */
public class Client {
    /**
     * 持有需要使用的原型接口对象
     */
    private Prototype prototype;

    /**
     * 构造方法,传入需要使用的原型接口对象
     *
     * @param prototype 需要使用的原型接口对象
     */
    public Client(Prototype prototype) {
        this.prototype = prototype;
    }

    /**
     * 示意方法,执行某个功能操作
     */
    public void operation() {
        //会需要创建原型接口的对象
        Prototype newPrototype = prototype.clone();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 2.4 重写示例

要使用原型模式来重写示例,先要在订单的接口上定义出克隆的接口,然后要求各个具体的订单对象克隆自身,这样就可以解决:在订单处理对象里面通过订单接口来创建新的订单对象的问题。

使用原型模式来重写示例的结构如图所示:

模型结构

1. 复制谁和谁来复制的问题

有了一个对象实例,要快速的创建跟它一样的实例,最简单的办法就是复制?这里又有两个小的问题:

复制谁呢?当然是复制这个对象实例,复制实例的意思是连带着数据一起复制。

谁来复制呢?应该让这个类的实例自己来复制,自己复制自己。

可是每个对象不会那么听话,自己去实现复制自己的。于是原型模式决定对这些对象实行强制要求,给这些对象定义一个接口,在接口里面定义一个方法,这个方法用来要求每个对象实现自己复制自己

由于现在存在订单的接口,因此就把这个要求克隆自身的方法定义在订单的接口里面,示例代码如下:

/**
 * 订单的接口,声明了可以克隆自身的方法
 */
public interface OrderApi {

    /**
     * 获取订单产品数量
     *
     * @return 订单中产品数量
     */
    int getOrderProductNum();

    /**
     * 设置订单产品数量
     *
     * @param num 订单产品数量
     */
    void setOrderProductNum(int num);

    /**
     * 克隆方法
     *
     * @return 订单原型的实例
     */
    OrderApi cloneOrder();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

2. 如何克隆

定义好了克隆的接口,那么在订单的实现类里面,就得让它实现这个接口,并具体的实现这个克隆方法,新的问题出来了,如何实现克隆呢?

很简单,只要先new一个自己对象的实例,然后把自己实例中的数据取出来,设置到新的对象实例中去,不就可以完成实例的复制了嘛,复制的结果就是有了一个跟自身一模一样的实例。

个人订单实现:(PersonalOrder.java)

/**
 * 个人订单对象
 */
public class PersonalOrder implements OrderApi {

    /**
     * 订购人员姓名
     */
    private String customerName;
    /**
     * 产品编号
     */
    private String productId;
    /**
     * 订单产品数量
     */
    private int orderProductNum = 0;

    @Override
    public int getOrderProductNum() {
        return this.orderProductNum;
    }

    @Override
    public void setOrderProductNum(int num) {
        this.orderProductNum = num;
    }

    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }

    public String getProductId() {
        return productId;
    }

    public void setProductId(String productId) {
        this.productId = productId;
    }

    public String toString() {
        return "本个人订单的订购人是=" + this.customerName + ",订购产品是=" + this.productId + ",订购数量为=" + this.orderProductNum;
    }

    @Override
    public OrderApi cloneOrder() {
        // 创建一个新的订单,然后把本实例的数据复制过去
        PersonalOrder order = new PersonalOrder();
        order.setCustomerName(this.customerName);
        order.setProductId(this.productId);
        order.setOrderProductNum(this.orderProductNum);

        return order;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

企业订单实现:(EnterpriseOrder.java)

/**
 * 企业订单对象
 */
public class EnterpriseOrder implements OrderApi {
    /**
     * 企业名称
     */
    private String enterpriseName;
    /**
     * 产品编号
     */
    private String productId;
    /**
     * 订单产品数量
     */
    private int orderProductNum = 0;

    @Override
    public int getOrderProductNum() {
        return this.orderProductNum;
    }

    @Override
    public void setOrderProductNum(int num) {
        this.orderProductNum = num;
    }

    public String getEnterpriseName() {
        return enterpriseName;
    }

    public void setEnterpriseName(String enterpriseName) {
        this.enterpriseName = enterpriseName;
    }

    public String getProductId() {
        return productId;
    }

    public void setProductId(String productId) {
        this.productId = productId;
    }

    public String toString() {
        return "本企业订单的订购企业是=" + this.enterpriseName + ",订购产品是=" + this.productId + ",订购数量为=" + this.orderProductNum;
    }

    @Override
    public OrderApi cloneOrder() {
        // 创建一个新的订单,然后把本实例的数据复制过去
        EnterpriseOrder order = new EnterpriseOrder();
        order.setEnterpriseName(this.enterpriseName);
        order.setProductId(this.productId);
        order.setOrderProductNum(this.orderProductNum);
        return order;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

3. 使用克隆方法

这里使用订单接口的克隆方法的,是订单的处理对象,也就是说,订单的处理对象就相当于原型模式结构中的Client。

当然,客户端在调用clone方法之前,还需要先获得相应的实例对象,有了实例对象,才能调用该实例对象的clone方法。

这里使用克隆方法的时候,跟标准的原型实现有一些不同,在标准的原型实现的示例代码里面,客户端是持有需要克隆的对象,而这里变化成了通过方法传入需要使用克隆的对象,这点需要注意。示例代码如下:

/**
 * 处理订单的业务对象
 */
public class OrderBusiness {

    /**
     * 创建订单的方法
     *
     * @param order 订单的接口对象
     */
    public void saveOrder(OrderApi order) {
        // 1. 判断当前的预定产品数量是否大于1000
        while (order.getOrderProductNum() > 1000) {
            // 2. 如果大于,还需要继续拆分
            // 2.1 再新建一份订单,跟传入的订单除了数量不一样外,其他都相同
            OrderApi newOrder = order.cloneOrder();
            // 然后进行赋予,产品数量为1000
            newOrder.setOrderProductNum(1000);

            // 2.2 原来的订单保留,把数量设置成减少1000
            order.setOrderProductNum(order.getOrderProductNum() - 1000);

            // 然后是业务功能处理,省略,打印输出
            System.out.println("拆分生成订单==" + newOrder);
        }
        // 3. 不超过,那就直接业务功能处理,省略,打印输出
        System.out.println("订单==" + order);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

4. 客户端

public class OrderClient {

    public static void main(String[] args) {
        // 创建订单对象,这里为了演示简单,直接new了
        PersonalOrder op = new PersonalOrder();
        // 设置订单数据
        op.setOrderProductNum(3123);
        op.setCustomerName("李四");
        op.setProductId("P0002");

        // 这里获取业务处理的类,也直接new了,为了简单,连业务接口都不能做
        OrderBusiness ob = new OrderBusiness();
        // 调用业务来保存订单对象
        ob.saveOrder(op);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行结果:

运行结果

# 三:模式讲解

# 3.1 认识原型模式

1. 原型模式的功能

原型模式的功能实际上包含两个方面:

一个是通过克隆来创建新的对象实例;

另一个是为克隆出来的新的对象实例复制原型实例属性的值;

原型模式要实现的主要功能就是:通过克隆来创建新的对象实例。一般来讲,新创建出来的实例的数据是和原型实例一样的。但是具体如何实现克隆,需要由程序自行实现,原型模式并没有统一的要求和实现算法。

2. 原型与new

原型模式从某种意义上说,就像是new操作,在前面的例子实现中,克隆方法就是使用new来实现的,但请注意,只是 "类似于new" 而不是 "就是new"。

克隆方法和new操作最明显的不同就在于:

new一个对象实例,一般属性是没有值的,或者是只有默认值;如果是克隆得到的一个实例,通常属性是有值的,属性的值就是原型对象实例在克隆的时候,原型对象实例的属性的值。

3. 原型实例和克隆的实例

原型实例和克隆出来的实例,本质上是不同的实例,克隆完成后,它们之间是没有关联的,如果克隆完成后,克隆出来的实例的属性的值发生了改变,是不会影响到原型实例的。即需使用深拷贝

# 3.2 深浅克隆

具体可以查看 深浅拷贝

# 3.3 优缺点

1. 对客户端隐藏具体的实现类型

原型模式的客户端,只知道原型接口的类型,并不知道具体的实现类型,从而减少了客户端对这些具体实现类型的依赖。

2. 在运行时动态改变具体的实现类型

原型模式可以在运行期间,由客户来注册符合原型接口的实现类型,也可以动态的改变具体的实现类型,看起来接口没有任何变化,但其实运行的已经是另外一个类实例了。因为克隆一个原型就类似于实例化一个类。

3. 深度克隆方法实现会比较困难

原型模式最大的缺点就在于每个原型的子类都必须实现clone的操作,尤其在包含引用类型的对象时,clone方法会比较麻烦,必须要能够递归的让所有的相关对象都要正确的实现克隆。

# 3.4 思考原型模式

1. 原型模式的本质

原型模式的本质:克隆生成对象。

克隆是手段,目的还是生成新的对象实例。正是因为原型的目的是为了生成新的对象实例,原型模式通常是被归类为创建型的模式

原型模式也可以用来解决 "只知接口而不知实现的问题",使用原型模式,可以出现一种独特的 "接口造接口" 的景象,这在面向接口编程中很有用。同样的功能也可以考虑使用工厂来实现。

另外,原型模式的重心还是在创建新的对象实例,至于创建出来的对象,其属性的值是否一定要和原型对象属性的值完全一样,这个并没有强制规定,只不过在目前大多数实现中,克隆出来的对象和原型对象的属性值是一样的。

也就是说,可以通过克隆来创造值不一样的实例,但是对象类型必须一样。可以有部分甚至是全部的属性的值不一样,可以有选择性的克隆,就当是标准原型模式的一个变形使用吧。

2. 何时选用原型模式

建议在如下情况中,选用原型模式:

如果一个系统想要独立于它想要使用的对象时,可以使用原型模式,让系统只面向接口编程,在系统需要新的对象的时候,可以通过克隆原型来得到;

如果需要实例化的类是在运行时刻动态指定时,可以使用原型模式,通过克隆原型来得到需要的实例;

# 3.5 相关模式

1. 原型模式和抽象工厂模式

功能上有些相似,都是用来获取一个新的对象实例的。

不同之处在于,原型模式的着眼点是在如何创造出实例对象来,最后选择的方案是通过克隆;而抽象工厂模式的着眼点则在于如何来创造产品簇,至于具体如何创建出产品簇中的每个对象实例,抽象工厂模式不是很关注

正是因为它们的关注点不一样,所以它们也可以配合使用,比如在抽象工厂模式里面,具体创建每一种产品的时候就可以使用该种产品的原型,也就是抽象工厂管产品簇,具体的每种产品怎么创建则可以选择原型模式。

2. 原型模式和生成器模式

这两种模式可以配合使用。

生成器模式关注的是构建的过程,而在构建的过程中,很可能需要某个部件的实例,那么很自然地就可以应用上原型模式,通过原型模式来得到部件的实例。

# 四:JDK

# 五:参考文献

最后更新: 10/19/2022, 12:31:23 AM