設計模式【4】-- 建造者模式詳解

語言: CN / TW / HK

開局一張圖,剩下全靠寫...

<img src="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/設計模式.png" style="zoom: 33%;" >

引言

設計模式集合: http://aphysia.cn/categories/...

如果你用過 Mybatis ,相信你對以下代碼的寫法並不陌生,先創建一個 builder 對象,然後再調用 .build() 函數:

InputStream is = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();

上面其實就是我們這篇文章所要講解的 建造者模式 ,下面讓我們一起來琢磨一下它。

什麼是建造者模式

建造者模式是設計模式的一種,將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。(來源於百度百科)

建造者模式,其實是創建型模式的一種,也是23種設計模式中的一種,從上面的定義來看比較模糊,但是不得不承認,當我們有能力用簡潔的話去定義一個東西的時候,我們才是真的瞭解它了,因為這個時候我們已經知道它的界限在哪。

所謂將一個複雜對象的構建與它的表示分離,就是將對象的構建器抽象出來,構造的過程一樣,但是不一樣的構造器可以實現不一樣的表示。

結構與例子

建造者模式主要分為以下四種角色:

Product
Bulider
ConcreteBuilder
Director

説到這裏,可能會有點懵,畢竟全都是定義,下面從實際例子來講講,就拿程序員最喜歡的電腦來説,假設現在要生產多種電腦,電腦有屏幕,鼠標,cpu,主板,磁盤,內存等等,我們可能立馬就能寫出來:

public class Computer {
    private String screen;
    private String mouse;
    private String cpu;
    private String mainBoard;
    private String disk;
    private String memory;
      ...
    public String getMouse() {
        return mouse;
    }

    public void setMouse(String mouse) {
        this.mouse = mouse;
    }

    public String getCpu() {
        return cpu;
    }

    public void setCpu(String cpu) {
        this.cpu = cpu;
    }
      ...
}

上面的例子中,每一種屬性都使用單獨的 set 方法,要是生產不同的電腦的不同部件,具體的實現還不太一樣,這樣一個類實現起來貌似不是很優雅,比如聯想電腦和華碩電腦的屏幕的構建過程不一樣,而且這些部件的構建,理論上都是電腦的一部分,我們可以考慮 流水線式 的處理。

當然,也有另外一種實現,就是多個構造函數,不同的構造函數帶有不同的參數,實現了可選的參數:

public class Computer {
    private String screen;
    private String mouse;
    private String cpu;
    private String mainBoard;
    private String disk;
    private String memory;

    public Computer(String screen) {
        this.screen = screen;
    }

    public Computer(String screen, String mouse) {
        this.screen = screen;
        this.mouse = mouse;
    }

    public Computer(String screen, String mouse, String cpu) {
        this.screen = screen;
        this.mouse = mouse;
        this.cpu = cpu;
    }
      ...
}

上面多種參數的構造方法,理論上滿足了按需構造的要求,但是還是會有不足的地方:

  • 倘若構造每一個部件的過程都比較複雜,那麼構造函數看起來就比較凌亂
  • 如果有多種按需構造的要求,構造函數就太多了
  • 構造不同的電腦類型,耦合在一塊,必須抽象出來

首先,我們先用流水線的方式,實現按需構造,不能重載那麼多構造函數:

public class Computer {
    private String screen;
    private String mouse;
    private String cpu;
    private String mainBoard;
    private String disk;
    private String memory;

    public Computer setScreen(String screen) {
        this.screen = screen;
        return this;
    }

    public Computer setMouse(String mouse) {
        this.mouse = mouse;
        return this;
    }

    public Computer setCpu(String cpu) {
        this.cpu = cpu;
        return this;
    }

    public Computer setMainBoard(String mainBoard) {
        this.mainBoard = mainBoard;
        return this;
    }

    public Computer setDisk(String disk) {
        this.disk = disk;
        return this;
    }

    public Computer setMemory(String memory) {
        this.memory = memory;
        return this;
    }
}

使用的時候,構造起來,就像是流水線一樣,一步一步構造就可以:

Computer computer = new Computer()
                .setScreen("高清屏幕")
                .setMouse("羅技鼠標")
                .setCpu("i7處理器")
                .setMainBoard("聯想主板")
                .setMemory("32G內存")
                .setDisk("512G磁盤");

但是以上的寫法不夠優雅,既然構造過程可能很複雜,為何不用一個特定的類來構造呢?這樣構造的過程和主類就分離了,職責更加清晰,在這裏內部類就可以了:

package designpattern.builder;

import javax.swing.*;

public class Computer {
    private String screen;
    private String mouse;
    private String cpu;
    private String mainBoard;
    private String disk;
    private String memory;

    Computer(Builder builder) {
        this.screen = builder.screen;
        this.cpu = builder.cpu;
        this.disk = builder.disk;
        this.mainBoard = builder.mainBoard;
        this.memory = builder.memory;
        this.mouse = builder.mouse;
    }

    public static class Builder {
        private String screen;
        private String mouse;
        private String cpu;
        private String mainBoard;
        private String disk;
        private String memory;

        public Builder setScreen(String screen) {
            this.screen = screen;
            return this;
        }

        public Builder setMouse(String mouse) {
            this.mouse = mouse;
            return this;
        }

        public Builder setCpu(String cpu) {
            this.cpu = cpu;
            return this;
        }

        public Builder setMainBoard(String mainBoard) {
            this.mainBoard = mainBoard;
            return this;
        }

        public Builder setDisk(String disk) {
            this.disk = disk;
            return this;
        }

        public Builder setMemory(String memory) {
            this.memory = memory;
            return this;
        }

        public Computer build() {
            return new Computer(this);
        }
    }
}

使用的時候,使用 builder 來構建,構建完成之後,調用build的時候,再將具體的值,賦予我們需要的對象(這裏是 Computer ):

public class Test {
    public static void main(String[] args) {
        Computer computer = new Computer.Builder()
                .setScreen("高清屏幕")
                .setMouse("羅技鼠標")
                .setCpu("i7處理器")
                .setMainBoard("聯想主板")
                .setMemory("32G內存")
                .setDisk("512G磁盤")
                .build();
        System.out.println(computer.toString());
    }
}

但是上面的寫法,如果我們構造多種電腦,每種電腦的配置不太一樣,構建的過程也不一樣,那麼我們就必須將構造器抽象出來,變成一個抽象類。

首先我們定義產品類 Computer

public class Computer {
    private String screen;
    private String mouse;
    private String cpu;

    public void setScreen(String screen) {
        this.screen = screen;
    }

    public void setMouse(String mouse) {
        this.mouse = mouse;
    }

    public void setCpu(String cpu) {
        this.cpu = cpu;
    }

    @Override
    public String toString() {
        return "Computer{" +
                "screen='" + screen + '\'' +
                ", mouse='" + mouse + '\'' +
                ", cpu='" + cpu + '\'' +
                '}';
    }
}

定義一個抽象的構造類,用於所有的電腦類構造:

public abstract class Builder {
    abstract Builder buildScreen(String screen);
    abstract Builder buildMouse(String mouse);
    abstract Builder buildCpu(String cpu);

    abstract Computer build();
}

先構造一台聯想電腦,那聯想電腦必須實現自己的構造器,每一款電腦總有自己特殊的地方:

public class LenovoBuilder extends Builder {
    private Computer computer = new Computer();

    @Override
    Builder buildScreen(String screen) {
        computer.setScreen(screen);
        return this;
    }

    @Override
    Builder buildMouse(String mouse) {
        computer.setMouse(mouse);
        return this;
    }

    @Override
    Builder buildCpu(String cpu) {
        computer.setCpu(cpu);
        return this;
    }

    @Override
    Computer build() {
        System.out.println("構建中...");
        return computer;
    }
}

構建器有了,還需要有個指揮者,它負責去構建我們具體的電腦:

public class Director {
    Builder builder = null;
    public Director(Builder builder){
        this.builder = builder;
    }
    
    public void doProcess(String screen,String mouse,String cpu){
        builder.buildScreen(screen)
                .buildMouse(mouse)
                .buildCpu(cpu); 
    }
}

使用的時候,我們只需要先構建 builder ,然後把 builder 傳遞給指揮者,他負責具體的構建,構建完之後,構建器調用一下 .build() 方法,就可以創建出一台電腦。

public class Test {
    public static void main(String[] args) {
        LenovoBuilder builder = new LenovoBuilder();
        Director director = new Director(builder);
        director.doProcess("聯想屏幕","遊戲鼠標","高性能cpu");
        Computer computer = builder.build();
        System.out.println(computer);
    }
}

打印結果:

構建中...
Computer{screen='聯想屏幕', mouse='遊戲鼠標', cpu='高性能cpu'}

以上其實就是完整的建造者模式,但是我們平時用的,大部分都是自己直接調用構建器 Builder ,一路 set() ,最後 build() ,就創建出了一個對象。

使用場景

構建這模式的好處是什麼?首先想到的應該是將構建的過程解耦了,構建的過程如果很複雜,單獨拎出來寫,清晰簡潔。其次,每個部分的構建,其實都是可以獨立去創建的,不需要多個構造方法,構建的工作交給了構建器,而不是對象本身。專業的人做專業的事。同樣,構建者模式也比較適用於不同的構造方法或者構造順序,可能會產生不同的構造結果的場景。

但是缺點還是有的,需要維護多出來的 Builder 對象,如果多種產品之間的共性不多,那麼抽象的構建器將會失去它該有的作用。如果產品類型很多,那麼定義太多的構建類來實現這種變化,代碼也會變得比較複雜。

最近在公司用 GRPC ,裏面的對象幾乎都是基於構建者模式,鏈式的構建確實寫着很舒服,也比較優雅,代碼是寫給人看的,我們所做的一切設計模式,都是為了拓展,解耦,以及避免代碼只能口口相傳。

【作者簡介】:

秦懷,公眾號【 秦懷雜貨店 】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向: Java源碼解析JDBCMybatisSpringredis分佈式劍指OfferLeetCode 等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裏胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查找資料。遺漏或者錯誤之處,還望指正。

劍指Offer全部題解PDF

2020年我寫了什麼?

開源編程筆記