设计模式之建造者模式

建造者模式

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

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

与工厂模式区别

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

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

推荐阅读更多精彩内容