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

秦懷雜貨店發表於2021-12-02

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

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

引言

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

如果你用過 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年我寫了什麼?

開源程式設計筆記

相關文章