天天看點

Java設計模式系列之建造者模式精講

作者:尚矽谷教育

在上篇文章中,咱們講解了幾種常見的設計模式,分别是單例模式、以及原型模式,這幾種作為建立型模式的常見模式,希望同學們能夠重點了解和掌握,無論是在技術還是程式設計思想上,相信對大家會帶來或多或少的幫助。

在本篇文章中将講解建立型模式的第三種模式,這也是開發中使用場景比較頻繁,相比之前幾種更重要的一種,也就是建造者模式。

不知道大家是不是有建房和買房的經驗,或者說沒吃過豬肉至少也見過豬跑,大家都知道房子有很多種,比如瓦房、平房、别墅還是海景房,它們雖然在品質上有着很大差距,但是在建房的整個過程,每個步驟都是相差無幾的。對一間房而言,建造過程都是選址,購買建築材料、打地基、澆築地梁、砌築牆體、封頂、裝修、入住。别問我建房過程怎麼知道的這麼詳細,我也不是專門設計房子的,問就是百度的,哈哈。

對房子來說不管是富麗堂皇的宮殿,還是依山傍水的别墅,甚至是普通人住的磚房瓦房,可以說都是經過這一堆繁雜的工序建造而成,隻不過這是每個步驟用得材料或者投入的money有很多的差距而已,而且對建房來說,它的每個步驟也是有着嚴格的順序的,而這一堆工序就像是組成建房的骨架,隻要是你要蓋房,都必須遵循這樣固定而形成的步驟,根據每種房子的不同,每個步驟自己可以進行比較個性化的設計,進而構造出自己滿意的成品。有了這些場景的支撐,下面呢,我們主要來研究一下這種場景對應的建造者模式。

一、建造者模式的介紹

概念:

建造者模式就是在這種情況出現的,一個類需要依賴很多元件組成,而不同對象每種元件的實作也會存在差異,最後将所有元件按照約定的規則組合起來的過程也就是建造者模式的設計核心。

像生活中的案例比比皆是,比如筆記本的生産,對于所有筆記本來說,不管你是多強悍的遊戲本,還是普普通通的辦公本,都是依賴于CPU、GPU以及記憶體條等元件構成的。隻不過不同價位的筆記本每個元件的品質和性能有差距罷了,比如MacBook Pro對于CPU而言,使用的是迄今為止地表第一的M2 MAX晶片,而聯想拯救者它的最高配CPU卻是13代英特爾酷睿i9處理器。

但是盡管它們每一個元件不一樣,但是最後裝配筆記本的時候裝配的過程和步驟都幾乎是相差無幾的。再比如作為程式員生産一個應用的過程,都會經曆提需求,解析需求,編寫接口文檔,項目正式開發,開發自測、短周期測試、sit測試、uat測試、投産演練測試、正式上線、後續維護這些複雜的過程。不管是百度還是其他公司的項目開發,幾乎都會經曆這些步驟,隻是每個步驟所負責的團隊會存在很大的差異而已。

在建造者模式中,主要用來實作複雜對象建構和裝配之間的解耦,不同的元件,相同的裝配,最後得出的複雜對象是不一樣的,比如:像小米筆記本以及蘋果筆記本,它們的裝配過程都是需要螢幕、CPU、顯示卡以及記憶體條,但是小米和蘋果各個元件使用的材料或者性能品質上肯定會有很大的差距,就導緻最後由這些元件組合而成的筆記本不一樣,我相信一台蘋果筆記本和小米筆記本放在你面前讓你二選一,大多數人必然會選擇蘋果筆記本吧。其次呢,相同的元件,使用不同的裝配順序也可以建立出不同的對象。

建造者模式,說白了就是将對象的裝配與元件的建立進行了分離,在建造者模式中,主要分為兩類角色:

第一是Builder,也就是建構器,主要用于對象所有的元件構造,在開發中,往往需要定義一個元件Builder父類抽象類,用于規定該複雜對象的建立需要依賴哪些元件。具體的産品需要哪些元件,建立相應的Builder對象進行實作就可以。

第二是Director,這個角色叫做指揮者或者裝配者,用于實作各個元件的裝配順序,就像工廠的流水線一般,一般有很多員工生産産品的各個元件,最後由專門的裝配勞工将其一一裝配形成最終的産品。

講了這麼一大堆理論,相信一些同學多多少少會有些雲裡霧裡,接下來通過一個場景案例來了解建造者模式這種比較重要的設計模式。

二、案例一

對于筆記本來說,它的生産需要依賴CPU、GPU以及記憶體條等元件,最後生産完成後同樣需要定價,而對于不同的筆記本而言它們的這些元件以及定價都會有很大的差距,比如主打高端的MacBook Pro,還有主打成本效益的小米Book,它們因為不同的定位針對這些元件的使用肯定是不一樣的。而且不管你是什麼樣的筆記本,其生産過程幾乎都是一模一樣的,比如一台筆記本需要先裝配CPU、其次GPU以及記憶體條,最後再進行定價,這些步驟并不能進行颠倒,比如你不能在筆記本還沒有生産出來之前就進行定價售賣。

那麼根據這個需求,作為開發者的你會怎麼進行設計開發呢,我相信大家最常見的一種思想就是通過繼承,可以定義一個筆記本的抽象父類,在這個父類裡定義填充CPU、GPU、記憶體條以及售價的抽象方法,每台具體的筆記本直接實作和重寫就可以了,同時定義一個組裝電腦的方法,用于維護每個元件的裝配順序即可。

下面呢,我們可以根據這種比較常見的是思想來設計這個案例:

第一步:建立筆記本電腦抽象類:

package com.ignorance.builder.book;

public abstract class AbstractBook {

public abstract void fillwithCpu();

public abstract void fillwithGpu();

public abstract void fillwithRam();

public abstract void fillWithPrice();

public void produceBook(){

fillwithCpu();

fillwithGpu();

fillwithRam();

fillWithPrice();

}

}

第二步:建立MacBookPro具體實作類

package com.ignorance.builder.book;

public class MacBookPro extends AbstractBook{

@Override

public void fillwithCpu() {

System.out.println("我是高端的MacBook Pro,我使用的是迄今為止地表最強的M2 MAX晶片,我可太快太牛逼了...");

}

@Override

public void fillwithGpu() {

System.out.println("我是高端的MacBook Pro,我使用的是迄今為止地表最強的M2 MAX GPU,對圖形這塊,我可是沒輸過...");

}

@Override

public void fillwithRam() {

System.out.println("我是高端的MacBook Pro,我搭配的固态記憶體8TB,你可以存各種各樣的東西,讀寫速度簡直太快了...");

}

@Override

public void fillWithPrice() {

System.out.println("我是高端的MacBook Pro,因為我很牛逼,是以價格為36999.00...");

}

}

第三步:建立小米筆記本實作類

package com.ignorance.builder.book;

public class XiaoMiBook extends AbstractBook {

@Override

public void fillwithCpu() {

System.out.println("我是小米筆記本,我的CPU是還不錯的13代英特爾酷睿i5處理器...");

}

@Override

public void fillwithGpu() {

System.out.println("我是小米筆記本,我的GPU是還不錯的Geforce GTX3050Ti...");

}

@Override

public void fillwithRam() {

System.out.println("我是小米筆記本,我的記憶體是固态512G,可能你存東西不能存太多,我的讀寫速度還是可以的...");

}

@Override

public void fillWithPrice() {

System.out.println("我是小米筆記本,我很便宜,隻需要4999.00,趕快帶我回家吧...");

}

}

第四步:建立LenvoBook實作類

package com.ignorance.builder.book;

public class LenvoBook extends AbstractBook{

@Override

public void fillwithCpu() {

System.out.println("我是聯想拯救者,我的CPU是13代英特爾酷睿i9處理器...");

}

@Override

public void fillwithGpu() {

System.out.println("我是聯想拯救者,我的GPU是Gerforce RTX4090Ti,很快很強...");

}

@Override

public void fillwithRam() {

System.out.println("我是聯想拯救者,我的固态記憶體為4TB...");

}

@Override

public void fillWithPrice() {

System.out.println("我是聯想拯救者,我的定價為25999.00...");

}

}

第五步:建立用戶端類,用于建構每一台電腦;

package com.ignorance.builder.book;

public class Client {

public static void main(String[] args) {

AbstractBook book = new MacBookPro();

book.produceBook();

System.out.println("====================");

book = new XiaoMiBook();

book.produceBook();

System.out.println("====================");

book = new LenvoBook();

book.produceBook();

}

}

以下是運作結果:

Java設計模式系列之建造者模式精講

可以看出使用繼承這種方式實作的效果還是不錯的,耦合度還是進行了一定的優化的,我之前公司基本用得也是這一種架構,但是這種架構會存在一定問題。在Client時,我們還是将對象的建立以及使用偶合在一起了,還是之前的規則,如果的對象發生了改變,那麼對的工程來說會是緻命的影響。接下來我們使用建造者模式進行改造,來感受一下兩者之間的差異。

三、案例二

使用建造者模式來模拟手機的生産,都知道手機是一種特别複雜的産品,如果不複雜,怎麼會有那麼多不入流的廠商會面臨倒閉呢。将手機進行細化,玩過手機的人,對手機稍微有一些了解的同學都應該知道,手機一般都是會由螢幕、CPU、主機闆、記憶體、相機、電池以及後蓋等元件組成。而每種手機的裝配規則幾乎相差無幾,都是将這些元件一一整合形成最終的産品。但是每種手機的這些元件差異性卻特别大,下面呢,使用建造者模式的思想來完成手機這個對象的建構。

在建造者模式中,一共包含如下角色:

抽象建造者類:也就是前面提過的Builder,這個類一般會将其設計為一個接口或者抽象類,一般用于規範某類對象需要生産哪些元件。

具體建造者類:用于實作Builder接口,完成複雜對象的各個元件的具體實作以及建構,在每個元件構造完成後傳回具體的産品執行個體。

産品類:表示将要建立的複雜對象。

指揮者類:也就是前面提到過的裝配類Director,主要用于調用相應builder用于完成各個元件的初始化,在此過程中,主要負責複雜對象各個元件的建立或者規定該對象各個元件按照什麼順序進行組合。

下面呢,針對手機這類對象,使用建造者設計模式的思想進行完成。

第一步:建立手機類

package com.ignorance.builder.phone;

public class MobilePhone {

//名稱

private String name;

//cpu

private String cpu;

//螢幕

private String screen;

//主機闆

private String mainBoard;

//電池

private String battery;

//後蓋

private String backCover;

//相機

private String camera;

//記憶體

private String memory;

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getCpu() {

return cpu;

}

public void setCpu(String cpu) {

this.cpu = cpu;

}

public String getScreen() {

return screen;

}

public void setScreen(String screen) {

this.screen = screen;

}

public String getMainBoard() {

return mainBoard;

}

public void setMainBoard(String mainBoard) {

this.mainBoard = mainBoard;

}

public String getBattery() {

return battery;

}

public void setBattery(String battery) {

this.battery = battery;

}

public String getBackCover() {

return backCover;

}

public void setBackCover(String backCover) {

this.backCover = backCover;

}

public String getCamera() {

return camera;

}

public void setCamera(String camera) {

this.camera = camera;

}

public String getMemory() {

return memory;

}

public void setMemory(String memory) {

this.memory = memory;

}

@Override

public String toString() {

return "MobilePhone{" +

"name='" + name + '\'' +

", cpu='" + cpu + '\'' +

", screen='" + screen + '\'' +

", mainBoard='" + mainBoard + '\'' +

", battery='" + battery + '\'' +

", backCover='" + backCover + '\'' +

", camera='" + camera + '\'' +

", memory='" + memory + '\'' +

'}';

}

}

在手機類中,定義了一些具體的屬性,用來模拟手機對象的建立需要依賴這些元件。剛才說過,此時手機各個元件的建立過程需要放在Builder類中。

第二步:定義Builder接口:

package com.ignorance.builder.phone;

public abstract class Builder {

protected MobilePhone mobilePhone = new MobilePhone();

public abstract void buildName();

public abstract void buildCpu();

public abstract void buildScreen();

public abstract void buildMainBoard();

public abstract void buildBattery();

public abstract void buildBackCover();

public abstract void buildCamera();

public abstract void buildMemory();

public abstract MobilePhone createPhone();

}

在Builder抽象類中,主要定義或者說聲明了手機這個複雜對象各個元件的建立方法;接下來呢,建立一系列具體對應的Builder實作類來完成對應元件的建立或者說實作。

第三步:建立IphoneBuilder類

package com.ignorance.builder.phone;

public class IphoneBuilder extends Builder{

@Override

public void buildName() {

mobilePhone.setName("iphone 14 pro max");

}

@Override

public void buildCpu() {

mobilePhone.setCpu("ios A16");

}

@Override

public void buildScreen() {

mobilePhone.setScreen("三星E5屏");

}

@Override

public void buildMainBoard() {

mobilePhone.setMainBoard("雙層主機闆");

}

@Override

public void buildBattery() {

mobilePhone.setBattery("4388毫安");

}

@Override

public void buildBackCover() {

mobilePhone.setBackCover("亞光質感玻璃背闆");

}

@Override

public void buildCamera() {

mobilePhone.setCamera("4800w像素");

}

@Override

public void buildMemory() {

mobilePhone.setMemory("8G+512GB");

}

@Override

public MobilePhone createPhone() {

return mobilePhone;

}

}

第四步:建立SanXingBuilder

package com.ignorance.builder.phone;

public class SanXingBuilder extends Builder{

@Override

public void buildName() {

mobilePhone.setName("三星S23 Ultra");

}

@Override

public void buildCpu() {

mobilePhone.setCpu("骁龍8gen2超頻版");

}

@Override

public void buildScreen() {

mobilePhone.setScreen("三星E7屏");

}

@Override

public void buildMainBoard() {

mobilePhone.setMainBoard("單層散熱主機闆");

}

@Override

public void buildBattery() {

mobilePhone.setBattery("5000毫安");

}

@Override

public void buildBackCover() {

mobilePhone.setBackCover("再生玻璃");

}

@Override

public void buildCamera() {

mobilePhone.setCamera("2億像素");

}

@Override

public void buildMemory() {

mobilePhone.setMemory("12G+1T");

}

@Override

public MobilePhone createPhone() {

return mobilePhone;

}

}

通過iphoneBuilder以及SanXingBuilder兩個實作類,将蘋果手機以及三星手機需要的具體元件建立出來了,接下來,還需要借助Director指揮類将這些元件組合在一起。

第五步:建立Director類:

package com.ignorance.builder.phone;

public class Director {

private Builder builder;

public Director(Builder builder) {

this.builder = builder;

}

public MobilePhone construct(){

builder.buildName();

builder.buildCpu();

builder.buildScreen();

builder.buildMainBoard();

builder.buildBattery();

builder.buildBackCover();

builder.buildCamera();

builder.buildMemory();

return builder.createPhone();

}

}

在Director類中,通過有參構造器注入一個具體的Builder,在核心方法construct()方法完成各個元件的具體組裝,并且傳回最後組裝出來的手機對象。

接下來呢,建立一個Client類進行測試:

package com.ignorance.builder.phone;

public class Client {

public static void main(String[] args) {

Director director = new Director(new IphoneBuilder());

MobilePhone iphone = director.construct();

System.out.println(iphone);

System.out.println("===========================");

director = new Director(new SanXingBuilder());

MobilePhone sanxing = director.construct();

System.out.println(sanxing);

}

}

運作結果如下圖所示:

Java設計模式系列之建造者模式精講

可以看出,使用了建造者模式最終的具體産品對象也就完成了建立,因為的對象相對比較簡單,一些同學覺得看不出差別,反而覺得麻煩,但是在開發中每種産品特别複雜,同時呢,其對應的元件也比較複雜,通過建造者模式,各個元件的建立過程以及裝配過程進行了解耦,在開發過程中進行了一定程度的解耦。

四、案例三

相信大家對于面向對象并不陌生,這時候有個問題,我們怎麼對一個對象的屬性進行指派呢?我相信這個時候大家都會說這還不簡單,要不調用屬性的set()方法,要不使用有參構造器。

比如對于手機這個類,定義有參構造器,如下所示:

public MobilePhone(String name, String cpu, String screen, String mainBoard, String battery, String backCover, String camera, String memory) {

this.name = name;

this.cpu = cpu;

this.screen = screen;

this.mainBoard = mainBoard;

this.battery = battery;

this.backCover = backCover;

this.camera = camera;

this.memory = memory;

}

然後呢,我們在用戶端建立對象時指派即可,如下所示:

MobilePhone mobilePhone = new MobilePhone("iphone 14 pro max","A16",

"三星E6","雙層主機闆","4399毫安","不鏽鋼磨砂",

"4800w像素","8+512GB");

我相信這種情況對大家來說很好了解,也是的基本操作了,簡直是太簡單不過了。但是呢,這種情況在開發中我相信大家都遇到過,當你看到别人聲明的方法有幾十個參數時,不知道你會有什麼感想,我相信絕大數小夥伴可能跟我一樣想罵人了,這種編碼方法維護起來太難太不友好呢。不要說維護了,一看到幾十個參數瞬間就沒有維護的欲望了。

接下來呢,我門同樣可以使用建造者的思想進行優化。

package com.ignorance.builder.phone;

public class MobilePhone {

//名稱

private String name;

//cpu

private String cpu;

//螢幕

private String screen;

//主機闆

private String mainBoard;

//電池

private String battery;

//後蓋

private String backCover;

//相機

private String camera;

//記憶體

private String memory;

private MobilePhone(Builder builder){

this.name = builder.name;

this.cpu = builder.cpu;

this.screen = builder.screen;

this.mainBoard = builder.mainBoard;

this.battery = builder.battery;

this.backCover = builder.backCover;

this.camera = builder.camera;

this.memory = builder.memory;

}

@Override

public String toString() {

return "MobilePhone{" +

"name='" + name + '\'' +

", cpu='" + cpu + '\'' +

", screen='" + screen + '\'' +

", mainBoard='" + mainBoard + '\'' +

", battery='" + battery + '\'' +

", backCover='" + backCover + '\'' +

", camera='" + camera + '\'' +

", memory='" + memory + '\'' +

'}';

}

public static final class Builder{

//名稱

private String name;

//cpu

private String cpu;

//螢幕

private String screen;

//主機闆

private String mainBoard;

//電池

private String battery;

//後蓋

private String backCover;

//相機

private String camera;

//記憶體

private String memory;

public Builder buildName(String name){

this.name = name;

return this;

}

public Builder buildCpu(String cpu){

this.cpu = cpu;

return this;

}

public Builder buildScreen(String screen){

this.screen = screen;

return this;

}

public Builder buildMainBoard(String mainBoard){

this.mainBoard = mainBoard;

return this;

}

public Builder buildBattery(String battery){

this.battery = battery;

return this;

}

public Builder buildBackCover(String backCover){

this.backCover = backCover;

return this;

}

public Builder buildCamera(String camera){

this.camera = camera;

return this;

}

public Builder buildMemory(String memory){

this.memory = memory;

return this;

}

public MobilePhone build(){

return new MobilePhone(this);

}

}

}

測試代碼如下:

package com.ignorance.builder.phone;

public class Client {

public static void main(String[] args) {

MobilePhone mobilePhone = new MobilePhone.Builder()

.buildName("三星S23 Ultra")

.buildCpu("骁龍8Gen2")

.buildBackCover("可再生玻璃")

.buildBattery("5000毫安")

.buildCamera("2億像素")

.buildMainBoard("單層散熱主機闆")

.buildMemory("12G+1TB")

.buildScreen("三星E7")

.build();

System.out.println(mobilePhone);

}

}

測試結果如下:

Java設計模式系列之建造者模式精講

可以看出這種編碼方法類似鍊式程式設計,我詳細有些同學或多或少都接觸過,像的Stream流、或者說MybatisPlus中的QueryMapper對象的建構都同樣使用到建造者模式這種思想,我相信大家使用到這種程式設計方式肯定會覺得維護起來更加輕松,而且編碼方式也相比而言較為優雅。通過在MobilePhone中定義了一個Builder的内部類進行屬性的封裝,我相信學過内部類的同學了解起來應該沒有多大難度。

總結

在今天這篇文章中,介紹了建立型模式的最後一種,也就是相比較而言更為重要的建造者模式,建造者模式的思想是将對複雜對象的元件建立以及組合進行了抽取和拆分,Builder專注元件的建立,Director指揮者專注元件的封裝傳回最終的目标對象。

設計模式是一門很強大的程式設計藝術,在後面呢,會接着把剩下的一系列設計模式進行一個整體的講解,希望對大家學習的過程有一定的幫助。

繼續閱讀