美文网首页
Builder pattern

Builder pattern

作者: Leocat | 来源:发表于2017-01-17 19:04 被阅读36次

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

相关文章

网友评论

      本文标题:Builder pattern

      本文链接:https://www.haomeiwen.com/subject/mmtvbttx.html