Builder pattern

Builder pattern

这里所介绍的建造者模式不是GOF中介绍的建造者模式。GOF中的建造者模式主要用于抽象构造的步骤,所以通过使用不同的建造者实现来获得不同的结果。
这里介绍的建造者模式是用于去除构造对象时不必要的复杂性,例如多构造器,多可选的参数和过度使用的Setter方法。

假设有一个User类,你想要将它设置为不可变的,而其中有些属性是必需的,而有些是可选的。所有的属性都是final的,所以只能在构造器中初始化。

public class User {
  private final String firstName;    //required
    private final String lastName;    //required
    private final int age;    //optional
    private final String phone;    //optional
    private final String address;    //optional
    ...
}

在我们还不了解建造者模式的时候,我们可能会这么写

public User(String firstName, String lastName) {
  this(firstName, lastName, 0);
}

public User(String firstName, String lastName, int age) {
  this(firstName, lastName, age, "");
}

public User(String firstName, String lastName, int age, String phone) {
  this(firstName, lastName, age, phone, "");
}

public User(String firstName, String lastName, int age, String phone, String address) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.age = age;
  this.phone = phone;
  this.address = address;
}

这个方式当然是正确的,但是这个方式的缺点也很明显。当你仅仅只有一点点的属性时,问题并不大,但是当
属性的数量增多时,代码就变得很难阅读和维护。更为重要的是,客户端地调用越来越难,客户端并不知道在那么多的
构造器中要选择哪一个。而且过多的参数也会让调用的人感到困惑,甚至于传错参数。(!- - 这个问题我也经常遇到)。

当然,我们也可以是使用JavaBeans约定,也就是有一个无参构造函数以及每个属性对应的Getter/Setter。如下:

public class User {
  private String firstName; // required
  private String lastName; // required
  private int age; // optional
  private String phone; // optional
  private String address;  //optional

  public String getFirstName() {
    return firstName;
  }
  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }
  public String getLastName() {
    return lastName;
  }
  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }
  public String getPhone() {
    return phone;
  }
  public void setPhone(String phone) {
    this.phone = phone;
  }
  public String getAddress() {
    return address;
  }
  public void setAddress(String address) {
    this.address = address;
  }
}

这个方式看起来很完美,我们创建一个空的对象,然后通过Setter来给我们需要的属性值设值。
但是这里有两个问题:

  1. 我们可能拥有一个状态不一致的实例。
  2. 该实例是可变的。

对于1来说,假设我们需要创建一个User对象,并且设置它的五个属性。那么在调用所有的五个Setter方法
来给属性设值之前,User对象没有一个完整的状态。也就是说,在构造该对象的过程中,客户应用的某些部分
可能会认为该对象已经构造完成,但是实际上并没有。

对于2来说,我们就对丢失不可变对象的优势,因为通过JavaBeans方式构造出来的对象都是可变的。

幸好我们还有第三种方式,通过建造者模式。

public class User {
  private final String firstName; // required
  private final String lastName; // required
  private final int age; // optional
  private final String phone; // optional
  private final String address; // optional

  private User(UserBuilder builder) {
    this.firstName = builder.firstName;
    this.lastName = builder.lastName;
    this.age = builder.age;
    this.phone = builder.phone;
    this.address = builder.address;
  }

  public String getFirstName() {
    return firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public int getAge() {
    return age;
  }

  public String getPhone() {
    return phone;
  }

  public String getAddress() {
    return address;
  }

  public static class UserBuilder {
    private final String firstName;
    private final String lastName;
    private int age;
    private String phone;
    private String address;

    public UserBuilder(String firstName, String lastName) {
      this.firstName = firstName;
      this.lastName = lastName;
    }

    public UserBuilder age(int age) {
      this.age = age;
      return this;
    }

    public UserBuilder phone(String phone) {
      this.phone = phone;
      return this;
    }

    public UserBuilder address(String address) {
      this.address = address;
      return this;
    }

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

有几个重要的点:

  1. User类的构造器是private的,所以客户端代码没办法直接实例化。
  2. 该类是不可变的(immutable)。所有的属性都是final的,都是通过构造器设置的。而且我们只提供了Getter
    方法,而没有Setter方法。
  3. 建造者使用了流式接口(Fluent Interface)形式来提高客户端代码的可读性。
  4. 建造者的构造器只要求必需的属性,而且被声明为final

通过建造者,客户端代码变得容易书写,最重要的是容易阅读。对该模式唯一的批评是,你不得不在建造者中重复类的属性。
但是因为建造者通常都是它所建造的类的静态成员类,所以将它们合并起来相当简单。

现在我们来看看如何使用它。

public User getUser() {
  return new
    User.UserBuilder("Jhon", "Doe")
    .age(30)
    .phone("1234567")
    .address("Fake address 1234")
    .build();
}

容易书写容易阅读,而且不管何时你获得该类的一个对象都不可能处于不一致的状态。

建造者模式非常地灵活。一个建造者可以创建多个对象,仅仅只需要在两次调用build方法之前更改建造者的属性。

很重要的一点是,就像构造器,建造者可以给它的参数强加不变性。build方法可以检查不变量,如果他们不是有效的就抛出IllegalStateException。在从建造者拷贝到对象之后再进行检查是很关键的,因为建造者是线程不安全的,当我们在真正地创建对象之前就检验参数,那么它们的值可能被另一个线程所改变。

public User build() {
  User user = new user(this);
  if (user.getAge() > 120) {
    throw new IllegalStateException(“Age out of range”); // thread-safe
  }
  return user;
}

因为我们先创建出user,然后再在不可变对象的基础上检验不变量,所以这个写法是线程安全的。
下面这个例子看起来功能相同,但是却是线程不安全的,应该避免这样写。

public User build() {
  if (age > 120) {
    throw new IllegalStateException(“Age out of range”); // bad, not thread-safe
  }
  // This is the window of opportunity for a second thread to modify the value of age
  return new User(this);
}

建造者模式的最后一个优势在于,建造者可以传给一个方法,使该方法可以创建一个或多个对象,而且该
方法并不需要知道任何关于对象创建的细节。为了实现这个目标,我们通常使用一个简单的接口,如下:

public interface Builder {
  T build();
}

在上面的例子中,我们可以让UserBuilder实现Builder,然后可以这样使用:

UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}

buildUserCollection方法中,我们直接调用userBuilderbuild方法来获取User,而不需要知道任何关于User对象创建的细节。

总结一下,建造者模式是一个非常棒的选择,特别是当一个类有多个参数(大于4个参数可能就是一个明显的特征),而且大多数的
参数是可选的。通过构造者模式,可以使客户端代码更容易阅读,书写和维护。而且,可以通过不可变性来使代码更加安全。

翻译自
The builder pattern in practice

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容