建造者模式——不止提高代碼逼格
點擊藍色“JavaKeeper”關(guān)注我喲加個“星標(biāo)”,一起成長,做牛逼閃閃的技術(shù)人
Keeper導(dǎo)讀:StringBuilder 你肯定用過,JDK 中的建造者模式
lombok 中的 @Bulider,你可能也用過,恩,這也是我們要說的建造者模式
簡介
Builder Pattern,中文翻譯為建造者模式或者構(gòu)建者模式,也有人叫它生成器模式。
建造者模式是一種創(chuàng)建型設(shè)計模式, 使你能夠分步驟創(chuàng)建復(fù)雜對象。它允許用戶只通過指定復(fù)雜對象的類型和內(nèi)容就可以構(gòu)建它們,用戶不需要知道內(nèi)部的具體構(gòu)建細節(jié)。
定義:將一個復(fù)雜對象的構(gòu)建與它的表示分離,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表示。
hello world
程序員麼,先上個 hello world 熱熱身
public class User {
private Long id;
private String name;
private Integer age; //可選
private String desc; //可選
private User(Builder builder) {
this.id = builder.id;
this.name = builder.name;
this.age = builder.age;
this.desc = builder.desc;
}
public static Builder newBuilder(Long id, String name) {
return new Builder(id, name);
}
public Long getId() {return id;}
public String getName() {return name;}
public Integer getAge() {return age;}
public String getDesc() {return desc;}
@Override
public String toString() {
return "Builder{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", desc='" + desc + '\'' +
'}';
}
public static class Builder {
private Long id;
private String name;
private Integer age;
private String desc;
private Builder(Long id, String name) {
Assert.assertNotNull("標(biāo)識不能為空",id);
Assert.assertNotNull("名稱不能為空",name);
this.id = id;
this.name = name;
}
public Builder age(Integer age) {
this.age = age;
return this;
}
public Builder desc(String desc) {
this.desc = desc;
return this;
}
public User build() {
return new User(this);
}
}
public static void main(String[] args) {
User user = User.newBuilder(1L, "starfish").age(22).desc("test").build();
System.out.println(user.toString());
}
}
這樣的代碼有什么優(yōu)缺點呢?
主要優(yōu)點:
- 明確了必填參數(shù)和可選參數(shù),在構(gòu)造方法中進行驗證;
- 可以定義為不可變類,初始化后屬性字段值不可變更;
- 賦值代碼可讀性較好,明確知道哪個屬性字段對應(yīng)哪個值;
- 支持鏈?zhǔn)椒椒ㄕ{(diào)用,相比于調(diào)用 Setter 方法,代碼更簡潔。
主要缺點:
- 代碼量較大,多定義了一個 Builder 類,多定義了一套屬性字段,多實現(xiàn)了一套賦值方法;
- 運行效率低,需要先創(chuàng)建 Builder 實例,再賦值屬性字段,再創(chuàng)建目標(biāo)實例,最后拷貝屬性字段。
當(dāng)然,以上代碼,就可以通過 Lombok 的 @Builder 簡化代碼
如果我們就那么三三兩兩個參數(shù),直接構(gòu)造函數(shù)配合 set 方法就能搞定的,就不用套所謂的模式了。
高射炮打蚊子——不合算
假設(shè)有這樣一個復(fù)雜對象, 在對其進行構(gòu)造時需要對諸多成員變量和嵌套對象進行繁復(fù)的初始化工作。這些初始化代碼通常深藏于一個包含眾多參數(shù)且讓人基本看不懂的構(gòu)造函數(shù)中;甚至還有更糟糕的情況, 那就是這些代碼散落在客戶端代碼的多個位置。
這時候才是構(gòu)造器模式上場的時候
上邊的例子,其實屬于簡化版的建造者模式,只是為了方便構(gòu)建類中的各個參數(shù),”正經(jīng)“的和這個有點差別,更傾向于用同樣的構(gòu)建過程分步創(chuàng)建不同的產(chǎn)品類。
我們接著扯~
結(jié)構(gòu)
從 UML 圖上可以看到有 4 個不同的角色
- 抽象建造者(Builder):創(chuàng)建一個 Produc 對象的各個部件指定的接口/抽象類
- 具體建造者(ConcreteBuilder):實現(xiàn)接口,構(gòu)建和裝配各個組件
- 指揮者/導(dǎo)演類(Director):構(gòu)建一個使用 Builder 接口的對象。負責(zé)調(diào)用適當(dāng)?shù)慕ㄔ煺邅斫M建產(chǎn)品,導(dǎo)演類一般不與產(chǎn)品類發(fā)生依賴關(guān)系,與導(dǎo)演類直接交互的是建造者類。
- 產(chǎn)品類(Product):一個具體的產(chǎn)品對象
demo
假設(shè)我是個汽車工廠,需求就是能造各種車(或者造電腦、造房子、做煎餅、生成不同文件TextBuilder、HTMLBuilder等等,都是一個道理)
1、生成器(Builder)接口聲明在所有類型生成器中通用的產(chǎn)品構(gòu)造步驟
public interface CarBuilder {
void setCarType(CarType type);
void setSeats(int seats);
void setEngine(Engine engine);
void setGPS(GPS gps);
}
2、具體的生成器(Concrete Builders)提供構(gòu)造過程的不同實現(xiàn)
public class SportsCarBuilder implements CarBuilder {
private CarType carType;
private int seats;
private Engine engine;
private GPS gps;
@Override
public void setCarType(CarType type) {
this.carType = type;
}
@Override
public void setSeats(int seats) {
this.seats = seats;
}
@Override
public void setEngine(Engine engine) {
this.engine = engine;
}
@Override
public void setGPS(GPS gps) {
this.gps = gps;
}
public Car getResult() {
return new Car(carType, seats, engine, gps);
}
}
3、產(chǎn)品(Products)是最終生成的對象
@Setter
@Getter
@ToString
public class Car {
private final CarType carType;
private final int seats;
private final Engine engine;
private final GPS gps;
private double fuel;
public Car(CarType carType,int seats,Engine engine,GPS gps){
this.carType = carType;
this.seats = seats;
this.engine = engine;
this.gps = gps;
}
}
4、主管(Director)類定義調(diào)用構(gòu)造步驟的順序,這樣就可以創(chuàng)建和復(fù)用特定的產(chǎn)品配置(Director 類的構(gòu)造函數(shù)的參數(shù)是 CarBuilder,但實際上沒有實例傳遞出去作參數(shù),因為 CarBuilder 是接口或抽象類,無法產(chǎn)生對象實例,實際傳遞的是 Builder 的子類,根據(jù)子類類型,決定生產(chǎn)內(nèi)容)
public class Director {
public void constructSportsCar(CarBuilder builder){
builder.setCarType(CarType.SPORTS_CAR);
builder.setSeats(2);
builder.setEngine(new Engine(2.0,0));
builder.setGPS(new GPS());
}
public void constructCityCar(CarBuilder builder){
builder.setCarType(CarType.CITY_CAR);
builder.setSeats(4);
builder.setEngine(new Engine(1.5,0));
builder.setGPS(new GPS());
}
public void constructSUVCar(CarBuilder builder){
builder.setCarType(CarType.SUV);
builder.setSeats(4);
builder.setEngine(new Engine(2.5,0));
builder.setGPS(new GPS());
}
}
5、客戶端使用(最終結(jié)果從建造者對象中獲取,主管并不知道最終產(chǎn)品的類型)
public class Client {
public static void main(String[] args) {
Director director = new Director();
SportsCarBuilder builder = new SportsCarBuilder();
director.constructSportsCar(builder);
Car car = builder.getResult();
System.out.println(car.toString());
}
}
適用場景
適用場景其實才是理解設(shè)計模式最重要的,只要知道這個業(yè)務(wù)場景需要什么模式,網(wǎng)上浪~程序員能不會嗎
使用建造者模式可避免重疊構(gòu)造函數(shù)的出現(xiàn)。
假設(shè)你的構(gòu)造函數(shù)中有 N 個可選參數(shù),那 new 各種實例的時候就很麻煩,需要重載構(gòu)造函數(shù)多次
當(dāng)你希望使用代碼創(chuàng)建不同形式的產(chǎn)品 (例如石頭或木頭房屋) 時, 可使用建造者模式。
如果你需要創(chuàng)建的各種形式的產(chǎn)品, 它們的制造過程相似且僅有細節(jié)上的差異, 此時可使用建造者模式。
使用生成器構(gòu)造組合樹或其他復(fù)雜對象。
建造者模式讓你能分步驟構(gòu)造產(chǎn)品。你可以延遲執(zhí)行某些步驟而不會影響最終產(chǎn)品。你甚至可以遞歸調(diào)用這些步驟, 這在創(chuàng)建對象樹時非常方便。
VS 抽象工廠
抽象工廠模式實現(xiàn)對產(chǎn)品家族的創(chuàng)建,一個產(chǎn)品家族是這樣的一系列產(chǎn)品:具有不同分類維度的產(chǎn)品組合,采用抽象工廠模式不需要關(guān)心抽象過程,只關(guān)心什么產(chǎn)品由什么工廠生產(chǎn)即可。而建造者模式則是要求按照指定的藍圖建造產(chǎn)品,它的主要目的是通過組裝零配件而生產(chǎn)一個新的產(chǎn)品。
最后
設(shè)計模式,這玩意看簡單的例子,肯定能看得懂,主要是結(jié)合自己的業(yè)務(wù)思考怎么應(yīng)用,讓系統(tǒng)設(shè)計更完善,懂了每種模式后,可以找找各種框架源碼或在 github 搜搜相關(guān)內(nèi)容,看看實際中是怎么應(yīng)用的。
公眾號回復(fù) ”設(shè)計模式“,領(lǐng)取 10 本設(shè)計模式 pdf 書籍
參考
- refactoringguru.cn

