专栏首页Java小白成长之路第22次文章:建造者模式+原型模式

第22次文章:建造者模式+原型模式

这周我们就可以把GOFO23设计模式中的创建型模式全部介绍完了!后面在项目里面可以试一下啦!

一、建造者模式

1、场景

(1)我们需要建造一个复杂的产品。比如:神州飞船,iPhone。这个复杂的产品的创建。有这样一个问题需要处理:装配这些子组件是不是有个步骤问题?

(2)实际开发中,我们所需要的对象构建有时也非常复杂,有许多步骤需要处理时。

2、本质

(1)分离了对象子组件的单独构造(由Builder来负责)和装配(由director负责)。从而可以构造出复杂的对象。这个模式适用于:某个对象的构造过程复杂的情况下使用。

(2)由于实现了构建和装配的解耦。不同的构造器,相同的装配,可以做出不同的对象;相同的构造器,不同的装配顺序也可以做出不同的对象,也就是实现了构建算法、装配算法的解耦,实现了更好的复用。

3、建造者模式实例化

(1)背景

现在我们假设一个背景,我们需要建造一个一艘飞船,里面需要用到3个零部件,分别是发动机,轨道舱和逃逸塔。首先我们需要构造一个飞船类,以及相应的发动机,轨道舱和逃逸塔类。对于一艘飞船,我们需要3个零部件。主要的实现思路如下:

public class AirShip {  private Engine engine;//发动机  private OrbitalModule orbitalModule;//轨道舱  private EscapeTower escapeTower;//逃逸塔    public Engine getEngine() {    return engine;  }  public void setEngine(Engine engine) {    this.engine = engine;  }  public OrbitalModule getOrbitalModule() {    return orbitalModule;  }  public void setOrbitalModule(OrbitalModule orbitalModule) {    this.orbitalModule = orbitalModule;  }  public EscapeTower getEscapeTower() {    return escapeTower;  }  public void setEscapeTower(EscapeTower escapeTower) {    this.escapeTower = escapeTower;  }}
class Engine{  private String name;  public Engine(String name) {    super();    this.name = name;  }  public String getName() {    return name;  }  public void setName(String name) {    this.name = name;  }}
class OrbitalModule{  ......}class EscapeTower{    ......}

tips:OrbitalModule和EscapeTower类中实现的代码与Engine中的代码类似,所以在上面的代码块中省略。

(2)builder和director的构建

在建造者模式中,最核心的部分就是将零部件的创造和组装进行分离,使得整个创建飞船的每一个步骤都相对简洁。所以我们根据建造飞船的流程,分别构建两个接口AirShipBuilder(负责零部件的构建)和AirShipDirector(负责零部件的组装)。

public interface AirShipBuilder {  OrbitalModule builderOrbitalModule();  Engine builderEngine();  EscapeTower builderEscapeTower();}
public interface AirShipDirector {  AirShip directAirShip();}

(3)实体化构建

经过上述的基本准备,已经将一个飞船类的全部流程节点规划好了,接下来我们通过上述流程,实现一个具体的飞船对象PengAirShip,依次构建PengAirShipBuilder和PengAirShipDirector。

public class PengAirShipBuilder implements AirShipBuilder{    @Override  public OrbitalModule builderOrbitalModule() {    System.out.println("建造轨道舱");    return new OrbitalModule("pengAirShip轨道舱");  }
  @Override  public Engine builderEngine() {    System.out.println("建造发动机");    return new Engine("pengAirShip发动机");  }
  @Override  public EscapeTower builderEscapeTower() {    System.out.println("建造逃逸塔");    return new EscapeTower("pengAirShip逃逸塔");  }}
public class PengAirShipDirector implements AirShipDirector {  private PengAirShipBuilder builder;  public PengAirShipDirector(PengAirShipBuilder builder) {    super();    this.builder = builder;  }      @Override  public AirShip directAirShip() {    AirShip airShip = new AirShip();    airShip.setEngine(builder.builderEngine());    airShip.setEscapeTower(builder.builderEscapeTower());    airShip.setOrbitalModule(builder.builderOrbitalModule());    return airShip;  }}

(4)完成构建

最后我们通过一个简单的测试,来实现PengAirShip构建的全部操作。

public class Client {  public static void main(String[] args) {    PengAirShipDirector a = new PengAirShipDirector(new PengAirShipBuilder());    AirShip airship = a.directAirShip();    System.out.println(airship.getEngine().getName());    System.out.println(airship.getEscapeTower().getName());    System.out.println(airship.getOrbitalModule().getName());  }}

测试结果:

tips:我们可以在结果中看出建造的过程信息,以及最后的各个零部件的信息。

4、总结

上周我们一起聊了聊工厂模式,我们现在回过来将两者进行一个对比。会感到两者甚至有点相似,都是将在满足客户端要求的前提下,尽可能的通过构建一系列的流程方法,简化客户端的操作步骤。在工厂模式中,我们通过构建一系列的工厂来实现生产一个实体类对象,相当于一个工厂同时兼任了零部件的构造以及零部件的组装两个工作。而建造者模式的优点就在于将构建和组装进行分离,互相解耦。建造者模式的核心思想在于将工厂的作用再次进行细化。一个工厂仅仅负责一个职能,使得对每个工厂的管理更加方便。相互的关联也更少。

二、原型模式prototype

1、场景

通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。

2、原型模式

(1)就是java中的克隆技术,以某个对象为原型,复制出新的对象。显然,新的对象具备原型对象的特点。

(2)优势:效率高(直接克隆,避免了重新执行构造过程步骤)。

(3)克隆类似于new,但是不同于new。new创建新的对象属性采用的是默认值。克隆出的对象的属性值完全和原型对象相同。并且克隆出的新对象改变不会影响原型对象。然后,再修改克隆对象的值。

3、原型模式实现

在原型模式中,我们需要在需要实现克隆的类上实现Cloneable接口,并且重写clone方法。所以我们首先构建一个Sheep类。

public class Sheep implements Cloneable{  private String name;  private Date birthday;    @Override  protected Object clone() throws CloneNotSupportedException {    return super.clone();  }
  public Sheep(String name, Date birthday) {    super();    this.name = name;    this.birthday = birthday;  }.....}

tips:省略号的地方是两个属性name 和 birthday的set与get方法。需要注意的是,方法clone()属于类Object的方法,而不是Cloneable接口中的方法。我们打开Cloneable接口的源码可以发现,此接口是一个空接口,里面没有任何方法。这也就代表着Cloneable接口仅仅属于一个标识符。

(1)浅复制

使用clone方法的时候,在JVM的内部,使用的并不是值的复制,而是将原生对象的属性地址传递给了新对象。稍微思考,这样的机制就会产生一个问题:假如我们将一个原生对象属性值的地址传递给一个新对象,然后该属性的值假如有所变化,那么紧接着,新对象该属性的值也会跟着变化,这样就无法和原对象的值保持一致了。这就是所谓的浅复制,我们结合下面的例子来说明。

public class Client {  public static void main(String[] args) throws CloneNotSupportedException {        Date date = new Date(351454652132654312L);    Sheep s1 = new Sheep("多利",date);    Sheep s2 = (Sheep) s1.clone();        System.out.println(s1);    System.out.println(s1.getName());    System.out.println(s1.getBirthday());
    date.setTime(546565454654635L);        System.out.println("多利羊的新出生日期:"+s1.getBirthday());    System.out.println("\n"+"###########################################"+"\n");    System.out.println(s2);    System.out.println(s2.getName());    System.out.println(s2.getBirthday());    System.out.println("\n"+"可以修改s2中的值");    s2.setName("邵莉");    System.out.println(s2.getName());  }}

我们查看一下结果:

tips:

1.首先我们看一下两个红色方框,我们在程序中打印出了两个类s1和s2,s2是使用clone()方法得到新对象。可以在红色方框中可以看出,两个对象s1和s2属于两个不同的对象,由此证明,clone()方法产生的是新对象,而不是对原对象的引用。

2.我们关注一下蓝色方框中的日期,在程序的最开始,我们随机设置了一个日期date,打印出来的日期为蓝色方框中的日期。并将其设置为原对象s1的birthday属性值。在完成克隆操作之后,我们对date值进行更改,可以发现,s2的birthday也同时改变为新的date值,与s1对象最开始的birthday值不同(橘色方框)。这种结果的原因就是克隆方法的基本原理是基于地址的复制,而并非真实值的复制。所以这就是浅复制。

(2)深复制

面对上面的浅复制,在很多情况下是不满足我们的需求的,既然是克隆,那么我们的需求应该是克隆出的对象,与原对象的所有值都是相同的,而不会随着外界属性值的改变而有所改变,这样才更加符合我们的需求。面对这样需求,我们需要在源头上进行克服。主要是在clone()方法的内部,对birthday进行再次克隆。所以我们在Sheep的基础上加以修改clone()方法,对Sheep的birthday属性进行克隆,由此就可以解决浅克隆问题。修改如下:

public class Sheep2 implements Cloneable,Serializable{  private String name;  private Date birthday;    @Override  protected Object clone() throws CloneNotSupportedException {    Object obj = super.clone();    Sheep2 s = (Sheep2) obj;    s.birthday = (Date) this.birthday.clone();//把属性也进行克隆    return s;  }    ......}

tips:Sheep2与Sheep的主要区别就在于clone()方法的修改,从Sheep仅仅克隆对象,到Sheep2中克隆属性,从而避免了浅克隆问题。

然后我们继续使用上面测试代码,检查一下修正过后的Sheep2类型。结果如下:

tips:我们观察上图中的红色方框,第一个红色方框表示s1最开始的birthday日期,s2表示date经过修改过后,s2的birthday日期。橘色方框表示birthday修改后的日期。可以发现,虽然date经过修改,但是s2的birthday并没有改变。这就是由于在date修改之前,s2克隆了s1中birthday的属性值,此时s2的birthday属性值就不再依赖于date中的值了。这就是深克隆的思想。

4、使用序列化和反序列化技术实现克隆

在前面的多次文章中,我们提到过序列化和反序列化的内容。就属于将二进制值的输入与输出。通过这种机制,我们也可以完成一个对象的克隆。具体的实现方案如下;

public class Client3 {  public static void main(String[] args) throws Exception {        Date date = new Date(351454652132654312L);    Sheep2 s1 = new Sheep2("多利",date);        System.out.println(s1);    System.out.println(s1.getName());    System.out.println(s1.getBirthday());    //    利用序列化和反序列化进行深复制    ByteArrayOutputStream bos = new ByteArrayOutputStream();    ObjectOutputStream  oos = new ObjectOutputStream(bos);        oos.writeObject(s1);    byte[] bytes = bos.toByteArray();        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);    ObjectInputStream ois = new ObjectInputStream(bis);    Sheep2 s2 = (Sheep2) ois.readObject();//通过序列化把s1的属性值都进行复制
//    重新设置时间    date.setTime(5465654546635L);    System.out.println("修改后的日期:"+s1.getBirthday());        System.out.println("\n"+"###########################################"+"\n");
    System.out.println(s2);    System.out.println(s2.getName());    System.out.println(s2.getBirthday());
    System.out.println("\n"+"可以修改s2中的值");    s2.setName("邵莉");    System.out.println(s2.getName());  }}

tips:序列化与反序列化实现的克隆,属于深克隆,所得结果与上面的深克隆结果一致,此处不再粘贴结果。仅仅将实现方法粘贴出来,作为一种对于克隆模式的思考和拓展。

5、原型模式中几个注意点

(1)原型模式产生新对象的方法的效率大大高于new方法产生新对象的效率,所以当需要产生大量类似对象时,优先考虑原型模式。

(2)原型模式主要是用来产生新对象,克隆之后得到的新对象,只是获得了原始对象全部特性,我们依旧可以根据自己的需求更改新对象的一些属性值。

(3)原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。

三、创建型模式的总结

创建型模式:都是用来帮助我们创建对象的

1、单例模式

保证一个类只有一个实例,并且提供一个访问该实例的全局访问点

2、工厂模式

(1)简单工厂模式:用来生产同一等级结构中的任意产品。(对于增加新的产品,需要修改已有代码)

(2)工厂方法模式:用来生产同一等级结构中的固定产品。(支持增加任意产品)

(3)抽象工厂模式:用来生产不同产品族的全部产品。(对于增加新的产品,无能为力;支持增加产品族)

3、建造者模式

分离了对象子组件的单独构造(由Builder来负责)和装配(由Director负责)。从而可以构造出复杂的对象。

4、原型模式

通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。

本文分享自微信公众号 - Java小白成长之路(Java_xiaobai),作者:鹏程万里

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 第25次文章:行为型模式

    关注系统中对象之间的相互交互,研究系统在运行时对象之间的相互通信和协作,进一步明确对象的职责,共有11种模式。

    鹏-程-万-里
  • 第8次文章:其他流

    使用方法:read(byte[] b,int off,int len) +close()

    鹏-程-万-里
  • 第49次文章:Filter&Listener

    每周一约!哈哈!小白又来啦!这周我们来聊聊Javaweb的三大组件——servlet、filter、listener。servlet之前的文章已经讲过了,这次来...

    鹏-程-万-里
  • Java原型模式(prototype)

      prototype模式也就是原型模式,是javaGOF23种设计模式中的一种,我们在学习spring的时候在bean标签的学习中碰到过,所以本文来给大家介绍...

    用户4919348
  • 来谈谈JAVA面向对象 - 鲁班即将五杀,大乔送他回家??

    剽悍一小兔
  • Java 对象释放与 finalize 方法

    本文谈论的知识很浅显,只是我发现自己掌握的相关知识并不扎实,对细节并不清楚,遂将疑惑解开,并记录于此。

    mzlogin
  • 深入理解Java8 Lambda表达式

    匿名函数的应用场景是: 通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用Lambda表达式。lambda表达式所表示的匿名函数的内容应该是很简单...

    ZhangXianSheng
  • 1.5 sleep()方法

    方法sleep()的作用是在指定的毫秒数内让当前"正在执行的线程"休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程...

    用户1134788
  • Java transient关键字使用小记

    Java学习123
  • Java一些常见的坑

    总是觉得自己Java基础还是不行,需要恶补。今天偶然mark了一本《Java解惑》,其中以端程序的方式罗列了95个即常见又不常见的xian(坑)jing(儿),...

    cxuan

扫码关注云+社区

领取腾讯云代金券