我所理解的设计模式系列·第3篇·你知道创建对象的最佳方式吗

2021/06/02 设计模式 共 5270 字,约 16 分钟

建造者模式(Builder Patter)属于创建型模式,它提供了一种创建对象的最佳方式。其定义是:”将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。“

design-patttern-3-建造者模式-封面

1. 模式介绍

建造者模式(Builder Patter)属于创建型模式,它提供了一种创建对象的最佳方式。其定义是:”将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。“

2. 应用场景

建造者模式主要解决的问题是如何更好地创建一个复杂对象

可能有人会说:如果我没有女朋友,new 一个就可以了,为什么还要基于建造者模式去创建一个“女朋友”呢?

带着这个问题,我们来试想一个场景:“女朋友”有很多属性,比如性别、年龄、发型、爱好等等等等,姑且就以这四个字段作为“女朋友”的属性吧。

当我们尝试去 new 一个“女朋友”的时候,首先确定一下需求:性别必须是女(否则就叫男朋友了),年龄得是正数,发型随便,爱好随便。

基于上述“女朋友”的需求,首先定义”女朋友“类:

public class GirlFriend {
    /** 性别 */
    private String sexual;
    /** 年龄 */
    private Integer age;
    /** 发型 */
    private String hire;
    /** 爱好 */
    private String hobby;
  
  	public GirlFriend() {
        this.sexual = "女";
    }
  
		public GirlFriend(Integer age, String hire, String hobby) {
        this.sexual = "女";
        if (age == null || age < 0) {
            throw new RuntimeException("年龄非法!");
        }
        this.age = age;
        this.hire = hire;
        this.hobby = hobby;
    }

		// 省略 getter or setter
}

然后通过最常用的构造器方式来 new 一个”女朋友“对象:

public static void main(String[] args) {
  GirlFriend myGirl = new GirlFriend(18, "长发及腰", "打球");
  System.out.println(myGirl);
}

从构造函数可以看到,入参顺序为:年龄、发型、爱好。看上去确实没啥问题,但是倘若属性再多些,就可能会导致参数列表会变得特别长,增加了代码的复杂度,影响可读性。同时也增加了出现“女朋友”的性别是“长发及腰”的情况,这样就不太善咯。

当然我们可以通过 set 方法的形式来解决上述问题,即另外一种常用的创建对象的方式,代码如下:

public static void main(String[] args) {
	GirlFriend myGirl = new GirlFriend();
  myGirl.setAge(18);
  myGirl.setHire("长发及腰");
  myGirl.setHobby("打球");
  System.out.println(myGirl);
}

// output: GirlFriend{sexual='女', age=18, hire='长发及腰', hobby='打球'}

相比以构造器形式来创建的方式来说,基于 set 方法的形式避免了构造函数参数列表过长可能导致的参数传递错误问题,但是它的弊端也显而易见:代码段冗杂。

综合上述两类问题,建造者模式应运而生。

3. 实现方式

实现建造者模式在原来的”女朋友“类中新增入参为 GirlFriendBuilder 的构造方法,同时新增一个建造者类 GirlFriendBuilder,如下所示:

public class GirlFriend {
  // 省略其他属性及方法
  
  	public GirlFriend(GirlFriendBuilder builder) {
        this.sexual = builder.getSexual();
        this.age = builder.getAge();
        this.hire = builder.getHire();
        this.hobby = builder.getHobby();
    }
}

// 建造者类
public class GirlFriendBuilder {
    /** 性别 */
    private String sexual;
    /** 年龄 */
    private Integer age;
    /** 发型 */
    private String hire;
    /** 爱好 */
    private String hobby;

    public GirlFriendBuilder setSexual(String sexual) {
        this.sexual = sexual;
        return this;
    }

    public GirlFriendBuilder setAge(Integer age) {
        this.age = age;
        return this;
    }

    public GirlFriendBuilder setHire(String hire) {
        this.hire = hire;
        return this;
    }

    public GirlFriendBuilder setHobby(String hobby) {
        this.hobby = hobby;
        return this;
    }

    // 省略 getter... 

    /**
     * build 方法,作为 GirlFriendBuilder -> GirlFriend 转换的方法,调用 GirlFriend 的全参构造方法
     * @return GirlFriend
     */
    public GirlFriend build() {
      return new GirlFriend(this);
    }
}

需要注意的是,其 setter 方法有些特殊,需要返回 GirlFriendBuilder 对象本身。除此之外,GirlFriendBuilder 对象还需要具备一个 build 方法,用以作为 GirlFriendBuilder 到 GirlFriend 映射方法,最后需要在 build 方法中调用的是 GirlFriend 类新增的参构造方法,返回 GirlFriend 对象。

这样,一个简单的建造者模式实现就完成了,下面是它的调用方式:

public static void main(String[] args) {
  GirlFriend newMyGirl = new GirlFriendBuilder().setSexual("女").setAge(18).setHire("长发及腰").setHobby("打球").build();
  System.out.println(newMyGirl);
}

// output: GirlFriend{sexual='女', age=18, hire='长发及腰', hobby='打球'}

4. 建造者模式拓展

除了以上构造“女朋友”的简单示例外,建造者模式还有很多其他的可拓展之处,下面就来逐一介绍。

4.1 定制化构建

首先要说的建造者模式的拓展点,是它的定制化构建能力。

套用上述创建“女朋友”的例子,我可以规定一些约束条件,例如必填属性校验、属性依赖关系约束等,以此来控制 GirlFriendBuilder 类的定制化构建对象能力。

举个例子,假设构建的约束条件是:性别和年龄是必填属性,如果设置了发型属性,那么她的爱好属性也需要设置。那么 GirlFriendBuilder 类的 build 方法应该是下面的样子:

public GirlFriend build() {
  // 性别、年龄必填
  if (this.sexual == null || this.age == null) {
    throw new IllegalArgumentException("GirlFriend sexual is null");
  }
  // 若设置发型属性,爱好属性也需要必填
  if (this.hire != null && this.hobby == null) {
    throw new IllegalArgumentException("GirlFriend hire is not null but hobby is null");
  }
  return new GirlFriend(this);
}

如果必填参数未设置,build 方法将会抛出异常,创建对象失败。

最后我们就可以利用建造者模式的定制化构建能力创建一个“定制化”的“女朋友了”:

public static void main(String[] args) {
  GirlFriend newMyGirl = new GirlFriendBuilder().setSexual("女").setAge(18).setHire("长发及腰").setHobby("打球").build();
  System.out.println(newMyGirl);
}

// output: GirlFriend{sexual='女', age=18, hire='长发及腰', hobby='打球'}

4.2 创建不可变对象

第二个要说的建造者模式的拓展点,就是创建不可变对象,即其属性在创建之后无法通过 set 方法进行改变。

这种场景的实现比较简单,只需要将原来 GirlFriend 类的 setter 方法访问修饰符改成 private 就可以了,只允许通过 new 来设置类的属性。

4.3 创建复杂对象

第三个要说的是基于建造者模式去创建复杂对象,与上述示例最大的不同就是”女朋友“类的属性并非简单类型,而是复杂类型,如:

public class ComplexGirlFriend {
    /**
     * 作为人的基本属性(年龄、姓名等)
     */
    private Person person;
    /**
     * 职业
     */
    private Profession profession;

  	public ComplexGirlFriend(ComplexGirlFriendBuilder complexGirlFriendBuilder) {
        this.person = complexGirlFriendBuilder.getPerson();
        this.profession = complexGirlFriendBuilder.getProfession();
    }
}

class Person {
    private Integer age;
    private String name;
  
  	// 省略 setter/getter 及构造方法
}

class Profession {
    private String professionName;
    private String professionType;
  
  	// 省略 setter/getter 及构造方法
}

// 建造者类
public class ComplexGirlFriendBuilder {

    private Person person;
    private Profession profession;

    public ComplexGirlFriendBuilder setPerson(Person person) {
        this.person = person;
        return this;
    }

    public ComplexGirlFriendBuilder setProfession(Profession profession) {
        this.profession = profession;
        return this;
    }

  	// 省略 getter... 

    public ComplexGirlFriend build() {
        return new ComplexGirlFriend(this);
    }
}

包含复杂属性的”女朋友”类拥有两个属性:基本属性类以及职业类。

建造者类 ComplexGirlFriendBuilder 并不关心基本属性类以及职业类的构建细节,它关注的只是目标创建对象 ComplexGirlFriend 类的构建细节。

当然基本属性 Person 类以及职业 Profession 类有可能也存在相应的构建者类,这里就不做递归讨论了。

也就是说,建造者类 ComplexGirlFriendBuilder 只关心传入的 Person 对象和 Profession 对象,并不关心这两个对象的构建细节,这样做的好处是将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。

5. 小结

基于本文对于建造者模式的描述(特别是 new 一个“女朋友”的过程),相信大家对建造者模式一定有很好的认识了,下面就来总结一下:基于建造者模式,开发者可以更好的创建一个复杂对象。

例如:避免出现构造函数参数列表过长导致的传参错误问题、属性过多 set 代码段冗杂问题等。

同时,开发者还可以对建造者模式进行拓展,使其具备定制化构建对象、创建不可变对象、创建复杂对象的能力。

在目前众多的开源产品中,构建者模式所用甚广。例如 MyBatis 中的 SqlSessionFactoryBuilder、OkHttp 中 Request 对象的构建、EventBus 的构建等等,使用的设计模式都是建造者模式。

6. 参考资料

最后,本文收录于个人语雀知识库: 我所理解的后端技术,欢迎来访。

文档信息

Search

    Table of Contents