建造者模式
建造者模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
为什么我们需要使用Builder模式?
在使用Builder模式之前,我们通常有以下两种方式创建一个带有属性的对象。
(1) 有参构造函数
(2) 无参构造函数 & set方法
以一个授信对象为例,我们来看以上两种方式。
首先定义一个对象
/**
* 授信结果
*/
public class CreditResult {
/**
* 授信生效时间 , 必填
*/
private LocalDate creditStartDate;
/**
* 授信过期时间 ,必填
*/
private LocalDate creditEndDate;
/**
* 授信额度 ,必填
*/
private BigDecimal availableQuota;
/**
* 客户手机号 ,必填
*/
private String customerPhone;
/**
* 客户邮箱号 , 非必填
*/
private String customerEmail;
}
拿到这样一个需求,我们立马就能写出两种代码:
/**
* 授信结果
*/
public class CreditResult {
/**
* 授信生效时间 , 必填
*/
private LocalDate creditStartDate;
/**
* 授信过期时间 ,必填
*/
private LocalDate creditEndDate;
/**
* 授信额度 ,必填
*/
private BigDecimal availableQuota;
/**
* 客户手机号 ,必填
*/
private String customerPhone;
/**
* 客户邮箱号 , 非必填
*/
private String customerEmail;
public CreditResult(LocalDate creditStartDate, LocalDate creditEndDate, BigDecimal availableQuota,
String customerPhone, String customerEmail) {
this.creditStartDate = creditStartDate;
this.creditEndDate = creditEndDate;
this.availableQuota = availableQuota;
this.customerPhone = customerPhone;
this.customerEmail = customerEmail;
}
}
public static void main(String[] args) {
CreditResult creditResult = new CreditResult(LocalDate.now(), LocalDate.now(), BigDecimal.valueOf(10000),
"13012345678", null);
}
/**
* 授信结果
*/
public class CreditResult {
/**
* 授信生效时间 , 必填
*/
private LocalDate creditStartDate;
/**
* 授信过期时间 ,必填
*/
private LocalDate creditEndDate;
/**
* 授信额度 ,必填
*/
private BigDecimal availableQuota;
/**
* 客户手机号 ,必填
*/
private String customerPhone;
/**
* 客户邮箱号 , 非必填
*/
private String customerEmail;
public void setCreditStartDate(LocalDate creditStartDate) {
this.creditStartDate = creditStartDate;
}
public void setCreditEndDate(LocalDate creditEndDate) {
this.creditEndDate = creditEndDate;
}
public void setAvailableQuota(BigDecimal availableQuota) {
this.availableQuota = availableQuota;
}
public void setCustomerPhone(String customerPhone) {
this.customerPhone = customerPhone;
}
public void setCustomerEmail(String customerEmail) {
this.customerEmail = customerEmail;
}
public static void main(String[] args) {
CreditResult creditResult = new CreditResult();
creditResult.setCreditStartDate(LocalDate.now());
creditResult.setCreditEndDate(LocalDate.now());
creditResult.setAvailableQuota(BigDecimal.valueOf(1000));
creditResult.setCustomerPhone("13012345678");
}
}
以上代码有几点需求不能同时满足:
(1)必填参数校验逻辑有处安放
(2)有依赖关系的校验逻辑有处安放(比如失效时间一定要大于开始时间)
(3)创建逻辑清晰,参数含义清晰(构造函数过长容易出错,参数含义需要看顺序)
(4)对象创建后无法修改
于是需要使用Builder模式。
/**
* 授信结果
*/
public class CreditResult {
/**
* 授信生效时间 , 必填
*/
private LocalDate creditStartDate;
/**
* 授信过期时间 ,必填
*/
private LocalDate creditEndDate;
/**
* 授信额度 ,必填
*/
private BigDecimal availableQuota;
/**
* 客户手机号 ,必填
*/
private String customerPhone;
/**
* 客户邮箱号 , 非必填
*/
private String customerEmail;
public static final class CreditResultBuilder {
private LocalDate creditStartDate;
private LocalDate creditEndDate;
private BigDecimal availableQuota;
private String customerPhone;
private String customerEmail;
private CreditResultBuilder() {}
public static CreditResultBuilder aCreditResult() { return new CreditResultBuilder(); }
public CreditResultBuilder withCreditStartDate(LocalDate creditStartDate) {
this.creditStartDate = creditStartDate;
return this;
}
public CreditResultBuilder withCreditEndDate(LocalDate creditEndDate) {
this.creditEndDate = creditEndDate;
return this;
}
public CreditResultBuilder withAvailableQuota(BigDecimal availableQuota) {
this.availableQuota = availableQuota;
return this;
}
public CreditResultBuilder withCustomerPhone(String customerPhone) {
this.customerPhone = customerPhone;
return this;
}
public CreditResultBuilder withCustomerEmail(String customerEmail) {
this.customerEmail = customerEmail;
return this;
}
public CreditResult build() {
Preconditions.checkNotNull(creditStartDate);
Preconditions.checkNotNull(creditEndDate);
Preconditions.checkNotNull(customerPhone);
Preconditions.checkNotNull(availableQuota);
Preconditions.checkArgument(creditEndDate.isAfter(creditStartDate));
CreditResult creditResult = new CreditResult();
creditResult.creditEndDate = this.creditEndDate;
creditResult.customerEmail = this.customerEmail;
creditResult.creditStartDate = this.creditStartDate;
creditResult.customerPhone = this.customerPhone;
creditResult.availableQuota = this.availableQuota;
return creditResult;
}
}
public static void main(String[] args) {
CreditResult creditResult = CreditResultBuilder.aCreditResult()
.withCreditStartDate(LocalDate.now())
.withCreditEndDate(LocalDate.now())
.withAvailableQuota(BigDecimal.valueOf(1000))
.withCustomerPhone("13012345678").build();
}
}
除此之外,Builder模式还可以避免无效对象出现在代码中。
public static void main(String[] args) {
CreditResult creditResult = new CreditResult();
creditResult.setCreditStartDate(LocalDate.now());
creditResult.setCreditEndDate(LocalDate.now());
}
上面这种代码,必填参数没有填完,此时对象处于无效状态,而Builder模式不会出现以上引用,创建出来的对象一定是有效的。
总结
实际上,如果我们并不是很关心对象是否有短暂的无效状态,也不是太在意对象是否是可变的。比如,对象只是用来映射数据库读出来的数据,那我们直接暴露 set() 方法来设置类的成员变量值是完全没问题的。
而且现在配合注解也是可以做到校验的,还可以结合lombok省略set方法,lombok也是可以设置accessor = true ,让set方法返回this引用,就可以链式创建了。
也可以直接使用Builder注解,省略Builder代码。
@Data
@Accessor(chain = true)
public class CreditResult {
/**
* 授信生效时间 , 必填
*/
@NotNull
private LocalDate creditStartDate;
/**
* 授信过期时间 ,必填
*/
@NotNull
private LocalDate creditEndDate;
/**
* 授信额度 ,必填
*/
@NotNull
private BigDecimal availableQuota;
/**
* 客户手机号 ,必填
*/
@NotNull
private String customerPhone;
/**
* 客户邮箱号 , 非必填
*/
private String customerEmail;
}
@Builder
@Getter
public class CreditResult {
/**
* 授信生效时间 , 必填
*/
@NotNull
private LocalDate creditStartDate;
/**
* 授信过期时间 ,必填
*/
@NotNull
private LocalDate creditEndDate;
/**
* 授信额度 ,必填
*/
@NotNull
private BigDecimal availableQuota;
/**
* 客户手机号 ,必填
*/
@NotNull
private String customerPhone;
/**
* 客户邮箱号 , 非必填
*/
private String customerEmail;
}
就设计意图来说,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。
(1)我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。
(2)如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。
(3)如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。
与工厂模式区别
创建意图上,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。