设计模式(三)之创建型模式-建造者模式&原型模式

一、建造者模式

定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

主要作用:在用户不知道对象的建造过程和细节的情况下,就可以直接创建复杂的对象

1、简单例子说明

1、工厂(建造者模式):负责制造汽车(组装过程和细节在工厂内)

2、汽车购买者(用户):用户只需要说出需要的型号(对象的类型和内容),然后直接购买就可以使用(不需要知道汽车是怎么组装的(车轮、车门、发动机、方向盘等等))

以造房为例:

​ 假设造房简化为如下步骤:

​ 1、地基

​ 2、钢筋工程

​ 3、铺电线

​ 4、粉刷

​ 则其流程基本可以概括为:建筑公司或工程承包商(指挥者)—>指挥工人(具体建造者)—>造房子(产品),最后验收

2、建筑者模式流程图

如下图所示,主要由指挥者指挥建造者来生产产品

假设下图中抽象的Builder为楼:则具体的Builder(工人)可以建造不同的产品,比如平房、别墅等等。

这也就对应了前面所说的:同样的构建过程可以创建不同的表示

img

3、代码实例(一)

3.1 Product类:产品类

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
//产品:房子
public class Product {

private String buildA;//地基
private String buildB;//钢筋工程
private String buildC;//铺电线
private String buildD;//粉刷

public String getBuildA() {
return buildA;
}

public void setBuildA(String buildA) {
this.buildA = buildA;
}

public String getBuildB() {
return buildB;
}

public void setBuildB(String buildB) {
this.buildB = buildB;
}

public String getBuildC() {
return buildC;
}

public void setBuildC(String buildC) {
this.buildC = buildC;
}

public String getBuildD() {
return buildD;
}

public void setBuildD(String buildD) {
this.buildD = buildD;
}

@Override
public String toString() {
return "Product{" +
"buildA='" + buildA + '\'' +
", buildB='" + buildB + '\'' +
", buildC='" + buildC + '\'' +
", buildD='" + buildD + '\'' +
'}';
}
}

3.2 Builder类:抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//抽象的建造者:方法
//abstract:该关键字用来表达抽象,抽象类不能被实例化,无法确定为一个具体的对象,
//abstract类不能使用final关键字修饰,因为final修饰的类无法被继承,而抽象类通过继承实现抽象方法
public abstract class Builder {


abstract void buildA();//地基
abstract void buildB();//钢筋工程
abstract void buildC();//铺电线0
abstract void buildD();//粉刷

//完工:得到产品
//抽象方法:没有自己的主体,不能用private修饰,因为抽象方法必须被子类实现,而private权限对于子类是无法访问的
//也不能用static修饰,static修饰的方法可以通过类名调用,而抽象方法没有主体,没有任何业务逻辑,调用也就毫无意义
abstract Product getProduct();
}

分析:

为什么要使用抽象类?

​ 楼房是千差万别的,楼房的外形、层数、内部房间的数量等等,但对于建造者来说,抽象出来的建筑流程是确定的,而每个流程实现的具体细节则是经常变化的

3.3 Worker类:工人类

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
//具体的建造者:工人
public class Worker extends Builder {
private Product product;
public Worker(){
product = new Product();//由工人来生产产品
}

@Override
void buildA() {
product.setBuildA("地基");
System.out.println("地基");
}

@Override
void buildB() {
product.setBuildB("钢筋工程");
System.out.println("钢筋工程");
}

@Override
void buildC() {
product.setBuildC("铺电线");
System.out.println("铺电线");
}

@Override
void buildD() {
product.setBuildD("粉刷");
System.out.println("粉刷");
}

@Override
Product getProduct() {
return product;
}
}

3.4 Director类:指挥类

1
2
3
4
5
6
7
8
9
10
11
//指挥:核心。负责指挥构建一个工程,工程如何构建,由它决定
public class Director {
//指挥工人按照顺序构建房子
public Product build(Builder builder){
builder.buildA();
builder.buildB();
builder.buildC();
builder.buildD();
return builder.getProduct();
}
}

3.5 Test类:测试类

1
2
3
4
5
6
7
8
9
10
//测试类
public class Test {
public static void main(String[] args) {
//指挥
Director director = new Director();
//指挥 具体的工人完成产品
Product build = director.build(new Worker());
System.out.println(build.toString());
}
}

结果:

1
2
3
4
5
地基
钢筋工程
铺电线
粉刷
Product{buildA='地基', buildB='钢筋工程', buildC='铺电线', buildD='粉刷'}

分析:

​ 在指挥类中,按照不同的顺序构建房子,则最终的输出结果不一样,因此能将一个复杂对象的构建与它的表示分离

​ 在测试类中,通过不同的工人,可以创造不同的房子,但其流程不变,也就是:使得同样的构建过程可以创建不同的表示

4、代码实例(二)

代码实例(一)是建造者模式的常规用法,指挥类(Director)在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但有些情况下需要简化系统结构,可以把Director和抽象建造者进行结合。

比如:麦当劳的套餐,服务员(具体建造者)可以随意搭配任意几种产品(零件)组成一款套餐(产品),然后出售给客户。比第一种方式少了指挥者,主要是因为第二种方式把指挥者交给用户来操作,使得产品的创建更加简单灵活。

4.1 Product类:产品类

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
//产品:套餐
public class Product {

private String BuildA = "汉堡";
private String BuildB = "可乐";
private String BuildC = "薯条";
private String BuildD = "甜点";

public String getBuildA() {
return BuildA;
}

public void setBuildA(String buildA) {
BuildA = buildA;
}

public String getBuildB() {
return BuildB;
}

public void setBuildB(String buildB) {
BuildB = buildB;
}

public String getBuildC() {
return BuildC;
}

public void setBuildC(String buildC) {
BuildC = buildC;
}

public String getBuildD() {
return BuildD;
}

public void setBuildD(String buildD) {
BuildD = buildD;
}

@Override
public String toString() {
return "Product{" +
"BuildA='" + BuildA + '\'' +
", BuildB='" + BuildB + '\'' +
", BuildC='" + BuildC + '\'' +
", BuildD='" + BuildD + '\'' +
'}';
}
}

4.2 Builder类:抽象类

1
2
3
4
5
6
7
8
9
//建造者
public abstract class Builder {
abstract Builder buildA(String msg);//汉堡
abstract Builder buildB(String msg);//可乐
abstract Builder buildC(String msg);//薯条
abstract Builder buildD(String msg);//甜点

abstract Product getProduct();
}

4.3 Worker类:建造类

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
//具体的建造者
public class Worker extends Builder {

private Product product;

public Worker(){
product = new Product();
}

@Override
Builder buildA(String msg) {
product.setBuildA(msg);
return this;
}

@Override
Builder buildB(String msg) {
product.setBuildB(msg);
return this;
}

@Override
Builder buildC(String msg) {
product.setBuildC(msg);
return this;
}

@Override
Builder buildD(String msg) {
product.setBuildD(msg);
return this;
}

@Override
Product getProduct() {
return product;
}
}

4.4 Test类:测试类

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
//服务员
Worker worker = new Worker();
//链式编程:在原来的基础上,可以自由组合,如果不组合,也有默认套餐
Product product = worker.buildA("全家桶").buildB("雪碧")
.getProduct();

System.out.println(product.toString());
}
}

结果:

1
Product{BuildA='全家桶', BuildB='雪碧', BuildC='薯条', BuildD='甜点'}

建造者模式优缺点

优点:

​ 1、产品的建造和表示分离,实现了解耦。使用建造者模式可以使客户端不必知道产品内部组成的细节。

​ 2、将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰。

​ 3、具体的建造者之间是相互独立的,这有利于系统的扩展。增加新的具体建造者无需修改原有类库的代码,符合“开闭原则”。

缺点:

​ 1、建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

​ 2、如果产品的内部变化复杂,可能会导致需要定义很多具体建造者来实现这种变化,导致系统变得很庞大。

建造者模式VS抽象工厂模式

1、与抽象工厂模式相比,建造者模式返回一个组装好的完整产品。而抽象工程模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品簇。

2、在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象。而在建造者模式中,客户端可以不直接调用建造者的相关方法,而是通过指挥者类来指导如何生成对象,报告对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一个完整的对象。

3、如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品簇的产品。那么建造者模式就是一个汽车组装工厂,通过对不同的组件可以返回一辆完整的汽车。

总结

产品类:一般是一个较为复杂的对象,也就是说创建对象的过程比较复杂,一般会有比较多的代码量。

抽象建造者:引入抽象建造者的目的,是为了将建造的具体过程交与它的子类来实现。这样更容易扩展。一般至少会有两个抽象方法,一个用来建造产品,一个用来返回产品。

建造者:实现抽象类的所有未实现的方法,具体来说一般是两项任务:组件产品,返回组建好的产品。

指挥类:负责调用适当的建造者来组建产品,指挥类一般不与产品类发生依赖关系,与指挥类交互的是建造者类。一般来说,指挥类被用来封装程序中易变的部分。

二、原型模式

原型模式用于创建重复的对象,同时又能保证性能。

1、代码实例

1.1 浅克隆

1.1.1 Video类—原型类

主要:

​ 1、实现一个接口:Cloneable

​ 2、重写一个方法: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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.util.Date;
/*
1、实现一个接口 Cloneable
2、重写一个方法 clone()
*/
public class Video implements Cloneable{
private String name;
private Date createDateTime;

//调用clone()方法时,需要抛出CloneNotSupportedException异常
@Override
protected Object clone() throws CloneNotSupportedException{
return super.clone();
}

public Video(){}

public Video(String name,Date createDateTime){
this.name = name;
this.createDateTime = createDateTime;
}

public String getName() {
return name;
}

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

public Date getCreateDateTime() {
return createDateTime;
}

public void setCreateDateTime(Date createDateTime) {
this.createDateTime = createDateTime;
}

@Override
public String toString() {
return "Video{" +
"name='" + name + '\'' +
", createDateTime=" + createDateTime +
'}';
}
}

1.1.2 Bilibili类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Date;

public class Bilibili {

public static void main(String[] args) throws CloneNotSupportedException {
//原型对象 v1
Date date = new Date();
Video v1 = new Video("狂神说Java",date);
System.out.println("v1=>"+v1);
System.out.println("v1=>hash:"+v1.hashCode());

// v1 克隆v2
//克隆出来的对象和原来是一模一样的
Video v2 = (Video) v1.clone();
System.out.println("v2=>"+v2);
System.out.println("v2=>hash:"+v2.hashCode());
}
}

结果:

1
2
3
4
v1=>Video{name='狂神说Java', createDateTime=Sat Jun 19 12:34:10 CST 2021}
v1=>hash:1735600054
v2=>Video{name='狂神说Java', createDateTime=Sat Jun 19 12:34:10 CST 2021}
v2=>hash:21685669

但这种初始简单克隆模式存在一个问题,即该模式是一个浅克隆。

浅克隆:新对象的基础类型的变量值与原对象相同,而特殊对象,即非八大基本类型的对象与原对象指向同一内存空间,不管新老对象谁对这段内存空间进行操作都会影响到另一个。

image-20210619124330093

如果将上诉的Bilibili类进行修改,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Date;
public class Bilibili {

public static void main(String[] args) throws CloneNotSupportedException {
//原型对象 v1
Date date = new Date();
Video v1 = new Video("狂神说Java",date);
//调用clone方法需要抛出异常CloneNotSupportedException
Video v2 = (Video) v1.clone(); // v1 克隆v2

System.out.println("v1=>"+v1);
System.out.println("v2=>"+v2);

System.out.println("=====================");
date.setTime(123465645);
System.out.println("v1=>"+v1);
System.out.println("v2=>"+v2);
}
}

结果:

1
2
3
4
5
v1=>Video{name='狂神说Java', createDateTime=Sat Jun 19 12:47:32 CST 2021}
v2=>Video{name='狂神说Java', createDateTime=Sat Jun 19 12:47:32 CST 2021}
=====================
v1=>Video{name='狂神说Java', createDateTime=Fri Jan 02 18:17:45 CST 1970}
v1=>Video{name='狂神说Java', createDateTime=Fri Jan 02 18:17:45 CST 1970}

分析:

​ 修改date的值后,发现v1和v2的date值全部改变,这与我们所期望的不符,即一个修改,不影响另一个值

1.2 深克隆

深克隆:新对象除了与老对象的八大基本类型的赋值一致以外,其类类型的对象在保证赋值一致的基础上,指向的是一段新的内存空间。

image-20210619220352293

通过修改clone方法的模式,实现深克隆

1.2.1 Video类—原型类

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
import java.util.Date;
/*
1、实现一个接口 Cloneable
2、重写一个方法 clone()
*/
public class Video implements Cloneable{
private String name;
private Date createDateTime;

@Override
protected Object clone() throws CloneNotSupportedException{
Object obj = super.clone();
//通过对子对象或子成员变量clone来实现深克隆~ 还可以通过序列化、反序列化实现深克隆
Video v = (Video) obj;
v.createDateTime = (Date) this.createDateTime.clone();//将这个对象的属性也进行克隆
return obj;
}

public Video(){}

public Video(String name, Date createDateTime){
this.name = name;
this.createDateTime = createDateTime;
}

public String getName() {
return name;
}

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

public Date getCreateDateTime() {
return createDateTime;
}

public void setCreateDateTime(Date createDateTime) {
this.createDateTime = createDateTime;
}

@Override
public String toString() {
return "Video{" +
"name='" + name + '\'' +
", createDateTime=" + createDateTime +
'}';
}
}

1.2.2 Bilibili类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Date;
public class Bilibili {

public static void main(String[] args) throws CloneNotSupportedException {
//原型对象 v1
Date date = new Date();
Video v1 = new Video("狂神说Java",date);
Video v2 = (Video) v1.clone(); // v1 克隆v2

System.out.println("v1=>"+v1);
System.out.println("v2=>"+v2);

System.out.println("=====================");
date.setTime(465478789);
System.out.println("v1=>"+v1);
System.out.println("v2=>"+v2);
}
}

结果:

1
2
3
4
5
v1=>Video{name='狂神说Java', createDateTime=Sat Jun 19 23:15:04 CST 2021}
v2=>Video{name='狂神说Java', createDateTime=Sat Jun 19 23:15:04 CST 2021}
=====================
v1=>Video{name='狂神说Java', createDateTime=Tue Jan 06 17:17:58 CST 1970}
v2=>Video{name='狂神说Java', createDateTime=Sat Jun 19 23:15:04 CST 2021}

分析:

​ 从结果中,就可以看出v1和v2的时间不同,因此,实现了原型的深克隆

序列化&反序列化

概念:

  • 序列化:把Java对象转换为字节序列的过程
  • 反序列化:把字节序列恢复为Java对象的过程

序列化用途:

  • 把对象得到的字节序列永久地保存到硬盘上,通常存放在一个文件中(持久化对象)
  • 在网络上传送对象的字节序列(网络传输对象)

序列化使用:必须实现Serializable接口

1.3 序列化实现深克隆

1.3.1 Video类—原型类

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
import java.io.*;
import java.util.Date;

public class Video implements Serializable {
private String name;
private Date createDateTime;

public Video(){ }

public Video(String name,Date createDateTime){
this.name = name;
this.createDateTime = createDateTime;
}

//深克隆:通过对象的序列化实现
public Object deepClone() throws IOException, ClassNotFoundException {
Video v = new Video(); //要克隆的对象
//序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos); //需要抛出IO异常
oos.writeObject(this);//将当前这个对象以对象流的方式输出

//反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Video v1 = (Video)ois.readObject();//需要抛出ClassNotFoundException异常
return v1;
}

public String getName() {
return name;
}

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

public Date getCreateDateTime() {
return createDateTime;
}

public void setCreateDateTime(Date createDateTime) {
this.createDateTime = createDateTime;
}

@Override
public String toString() {
return "Video{" +
"name='" + name + '\'' +
", createDateTime=" + createDateTime +
'}';
}
}

1.3.2 Bilibili类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.IOException;
import java.util.Date;

public class Bilibili {

public static void main(String[] args) throws IOException, ClassNotFoundException {
//原型对象 v1
Date date = new Date();

Video v1 = new Video("ldg原型",date);
Video v2 = (Video)v1.deepClone(); //需要抛出IOException, ClassNotFoundException异常

System.out.println("v1=>"+v1);
System.out.println("v2=>"+v2);

System.out.println("======================");
date.setTime(45547456);
System.out.println("v1=>"+v1);
System.out.println("v2=>"+v2);
}
}

结果:

1
2
3
4
5
v1=>Video{name='ldg原型', createDateTime=Tue Jun 22 22:35:27 CST 2021}
v2=>Video{name='ldg原型', createDateTime=Tue Jun 22 22:35:27 CST 2021}
======================
v1=>Video{name='ldg原型', createDateTime=Thu Jan 01 20:39:07 CST 1970}
v2=>Video{name='ldg原型', createDateTime=Tue Jun 22 22:35:27 CST 2021}

分析:

​ 通过序列化与反序列化的方式,也可以实现深克隆

总结

  • 浅克隆:克隆对象的值相同,且克隆对象指向的内存空间也相同

    可通过实现Cloneable接口和重写clone方法实现

  • 深克隆:克隆对象的值相同,且克隆对象指向的内存空间不相同

    1. 实现Cloneable接口和重写clone方法实现

      在重写clone方法时,对子对象也需要克隆。比如A1包含A2,要想真正实现A1的深克隆,则还需要克隆A2

    2. 通过序列化和反序列化的方式实现克隆

      需要实现Serializable接口

      • 序列化
      1
      2
      3
      4
      //序列化
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream(bos); //需要抛出IO异常
      oos.writeObject(this);//将当前这个对象以对象流的方式输出
      • 反序列化

        1
        2
        3
        4
        //反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        Video v1 = (Video)ois.readObject();//需要抛出ClassNotFoundException异常

参考

0%