设计模式之建造者模式

建造者模式

建造者模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。

为什么我们需要使用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() 方法来设置属性值的方式就不适用了。

与工厂模式区别

创建意图上,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。

你可能感兴趣的:(设计模式之建造者模式)