關(guān)于 Java 的靜態(tài)工廠方法,看這一篇就夠了!

小提示:閱讀本文大約需15~20分鐘。

本文略長(zhǎng),所以先來個(gè)內(nèi)容提要

    1. 序:什么是靜態(tài)工廠方法
    1. Effective Java
    • 2.1 靜態(tài)工廠方法與構(gòu)造器不同的第一優(yōu)勢(shì)在于,它們有名字
    • 2.2 第二個(gè)優(yōu)勢(shì),不用每次被調(diào)用時(shí)都創(chuàng)建新對(duì)象
    • 2.3 第三個(gè)優(yōu)勢(shì),可以返回原返回類型的子類
    • 2.4 第四個(gè)優(yōu)勢(shì),在創(chuàng)建帶泛型的實(shí)例時(shí),能使代碼變得簡(jiǎn)潔
    1. 除此之外
      3.1 可以有多個(gè)參數(shù)相同但名稱不同的工廠方法
      3.2 可以減少對(duì)外暴露的屬性
      3.3 多了一層控制,方便統(tǒng)一修改
    1. 總結(jié)

1. 序:什么是靜態(tài)工廠方法

在 Java 中,獲得一個(gè)類實(shí)例最簡(jiǎn)單的方法就是使用 new 關(guān)鍵字,通過構(gòu)造函數(shù)來實(shí)現(xiàn)對(duì)象的創(chuàng)建。
就像這樣:

    Fragment fragment = new MyFragment();
    // or
    Date date = new Date();

不過在實(shí)際的開發(fā)中,我們經(jīng)常還會(huì)見到另外一種獲取類實(shí)例的方法:

    Fragment fragment = MyFragment.newIntance();
    // or 
    Calendar calendar = Calendar.getInstance();
    // or 
    Integer number = Integer.valueOf("3");

↑ 像這樣的:不通過 new,而是用一個(gè)靜態(tài)方法來對(duì)外提供自身實(shí)例的方法,即為我們所說的靜態(tài)工廠方法(Static factory method)。

知識(shí)點(diǎn):new 究竟做了什么?

簡(jiǎn)單來說:當(dāng)我們使用 new 來構(gòu)造一個(gè)新的類實(shí)例時(shí),其實(shí)是告訴了 JVM 我需要一個(gè)新的實(shí)例。JVM 就會(huì)自動(dòng)在內(nèi)存中開辟一片空間,然后調(diào)用構(gòu)造函數(shù)來初始化成員變量,最終把引用返回給調(diào)用方。

2. Effective Java

在關(guān)于 Java 中書籍中,《Effective Java》絕對(duì)是最負(fù)盛名幾本的之一,在此書中,作者總結(jié)了幾十條改善 Java 程序設(shè)計(jì)的金玉良言。其中開篇第一條就是『考慮使用靜態(tài)工廠方法代替構(gòu)造器』,關(guān)于其原因,作者總結(jié)了 4 條(第二版),我們先來逐個(gè)看一下。

2.1 靜態(tài)工廠方法與構(gòu)造器不同的第一優(yōu)勢(shì)在于,它們有名字

由于語言的特性,Java 的構(gòu)造函數(shù)都是跟類名一樣的。這導(dǎo)致的一個(gè)問題是構(gòu)造函數(shù)的名稱不夠靈活,經(jīng)常不能準(zhǔn)確地描述返回值,在有多個(gè)重載的構(gòu)造函數(shù)時(shí)尤甚,如果參數(shù)類型、數(shù)目又比較相似的話,那更是很容易出錯(cuò)。

比如,如下的一段代碼 :

Date date0 = new Date();
Date date1 = new Date(0L);
Date date2 = new Date("0");
Date date3 = new Date(1,2,1);
Date date4 = new Date(1,2,1,1,1);
Date date5 = new Date(1,2,1,1,1,1);

—— Date 類有很多重載函數(shù),對(duì)于開發(fā)者來說,假如不是特別熟悉的話,恐怕是需要猶豫一下,才能找到合適的構(gòu)造函數(shù)的。而對(duì)于其他的代碼閱讀者來說,估計(jì)更是需要查看文檔,才能明白每個(gè)參數(shù)的含義了。

(當(dāng)然,Date 類在目前的 Java 版本中,只保留了一個(gè)無參和一個(gè)有參的構(gòu)造函數(shù),其他的都已經(jīng)標(biāo)記為 @Deprecated 了)

而如果使用靜態(tài)工廠方法,就可以給方法起更多有意義的名字,比如前面的 valueOf、newInstance、getInstance 等,對(duì)于代碼的編寫和閱讀都能夠更清晰。

2.2 第二個(gè)優(yōu)勢(shì),不用每次被調(diào)用時(shí)都創(chuàng)建新對(duì)象

這個(gè)很容易理解了,有時(shí)候外部調(diào)用者只需要拿到一個(gè)實(shí)例,而不關(guān)心是否是新的實(shí)例;又或者我們想對(duì)外提供一個(gè)單例時(shí) —— 如果使用工廠方法,就可以很容易的在內(nèi)部控制,防止創(chuàng)建不必要的對(duì)象,減少開銷。

在實(shí)際的場(chǎng)景中,單例的寫法也大都是用靜態(tài)工廠方法來實(shí)現(xiàn)的。

如果你想對(duì)單例有更多了解,可以看一下這里:?《Hi,我們?cè)賮砹囊涣腏ava的單例吧》

2.3 第三個(gè)優(yōu)勢(shì),可以返回原返回類型的子類

這條不用多說,設(shè)計(jì)模式中的基本的原則之一——『里氏替換』原則,就是說子類應(yīng)該能替換父類。
顯然,構(gòu)造方法只能返回確切的自身類型,而靜態(tài)工廠方法則能夠更加靈活,可以根據(jù)需要方便地返回任何它的子類型的實(shí)例。

Class Person {
    public static Person getInstance(){
        return new Person();
        // 這里可以改為 return new Player() / Cooker()
    }
}
Class Player extends Person{
}
Class Cooker extends Person{
}

比如上面這段代碼,Person 類的靜態(tài)工廠方法可以返回 Person 的實(shí)例,也可以根據(jù)需要返回它的子類 Player 或者 Cooker。(當(dāng)然,這只是為了演示,在實(shí)際的項(xiàng)目中,一個(gè)類是不應(yīng)該依賴于它的子類的。但如果這里的 getInstance () 方法位于其他的類中,就更具有的實(shí)際操作意義了)

2.4 第四個(gè)優(yōu)勢(shì),在創(chuàng)建帶泛型的實(shí)例時(shí),能使代碼變得簡(jiǎn)潔

這條主要是針對(duì)帶泛型類的繁瑣聲明而說的,需要重復(fù)書寫兩次泛型參數(shù):

Map<String,Date> map = new HashMap<String,Date>();

不過自從 java7 開始,這種方式已經(jīng)被優(yōu)化過了 —— 對(duì)于一個(gè)已知類型的變量進(jìn)行賦值時(shí),由于泛型參數(shù)是可以被推導(dǎo)出,所以可以在創(chuàng)建實(shí)例時(shí)省略掉泛型參數(shù)。

Map<String,Date> map = new HashMap<>();

所以這個(gè)問題實(shí)際上已經(jīng)不存在了。

3. 除此之外

以上是《Effective Java》中總結(jié)的幾條應(yīng)該使用靜態(tài)工廠方法代替構(gòu)造器的原因,如果你看過之后仍然猶豫不決,那么我覺得可以再給你更多一些理由 —— 我個(gè)人在項(xiàng)目中是大量使用靜態(tài)工廠方法的,從我的實(shí)際經(jīng)驗(yàn)來世,除了上面總結(jié)的幾條之外,靜態(tài)工廠方法實(shí)際上還有更多的優(yōu)勢(shì)。

3.1 可以有多個(gè)參數(shù)相同但名稱不同的工廠方法

構(gòu)造函數(shù)雖然也可以有多個(gè),但是由于函數(shù)名已經(jīng)被固定,所以就要求參數(shù)必須有差異時(shí)(類型、數(shù)量或者順序)才能夠重載了。
舉例來說:

class Child{
    int age = 10;
    int weight = 30;
    public Child(int age, int weight) {
        this.age = age;
        this.weight = weight;
    }
    public Child(int age) {
        this.age = age;
    }
}

Child 類有 age 和 weight 兩個(gè)屬性,如代碼所示,它已經(jīng)有了兩個(gè)構(gòu)造函數(shù):Child(int age, int weight) 和 Child(int age),這時(shí)候如果我們想再添加一個(gè)指定 wegiht 但不關(guān)心 age 的構(gòu)造函數(shù),一般是這樣:

public Child( int weight) {
    this.weight = weight;
}

↑ 但要把這個(gè)構(gòu)造函數(shù)添加到 Child 類中,我們都知道是行不通的,因?yàn)?java 的函數(shù)簽名是忽略參數(shù)名稱的,所以 Child(int age)Child(int weight) 會(huì)沖突。

這時(shí)候,靜態(tài)工廠方法就可以登場(chǎng)了。

class Child{
    int age = 10;
    int weight = 30;
    public static Child newChild(int age, int weight) {
        Child child = new Child();
        child.weight = weight;
        child.age = age;
        return child;
    }
    public static Child newChildWithWeight(int weight) {
        Child child = new Child();
        child.weight = weight;
        return child;
    }
    public static Child newChildWithAge(int age) {
        Child child = new Child();
        child.age = age;
        return child;
    }
}

其中的 newChildWithWeightnewChildWithAge,就是兩個(gè)參數(shù)類型相同的的方法,但是作用不同,如此,就能夠滿足上面所說的類似Child(int age)Child(int weight)同時(shí)存在的需求。
(另外,這兩個(gè)函數(shù)名字也是自描述的,相對(duì)于一成不變的構(gòu)造函數(shù)更能表達(dá)自身的含義,這也是上面所說的第一條優(yōu)勢(shì) —— 『它們有名字』)

3.2 可以減少對(duì)外暴露的屬性

軟件開發(fā)中有一條很重要的經(jīng)驗(yàn):對(duì)外暴露的屬性越多,調(diào)用者就越容易出錯(cuò)。所以對(duì)于類的提供者,一般來說,應(yīng)該努力減少對(duì)外暴露屬性,從而降低調(diào)用者出錯(cuò)的機(jī)會(huì)。

考慮一下有如下一個(gè) Player 類:

// Player : Version 1
class Player {
    public static final int TYPE_RUNNER = 1;
    public static final int TYPE_SWIMMER = 2;
    public static final int TYPE_RACER = 3;
    protected int type;
    public Player(int type) {
        this.type = type;
    }
}

Player 對(duì)外提供了一個(gè)構(gòu)造方法,讓使用者傳入一個(gè) type 來表示類型。那么這個(gè)類期望的調(diào)用方式就是這樣的:

    Player player1 = new Player(Player.TYPE_RUNNER);
    Player player2 = new Player(Player.TYPE_SWEIMMER);

但是,我們知道,提供者是無法控制調(diào)用方的行為的,實(shí)際中調(diào)用方式可能是這樣的:

    Player player3 = new Player(0);
    Player player4 = new Player(-1);
    Player player5 = new Player(10086);

提供者期望的構(gòu)造函數(shù)傳入的值是事先定義好的幾個(gè)常量之一,但如果不是,就很容易導(dǎo)致程序錯(cuò)誤。

—— 要避免這種錯(cuò)誤,使用枚舉來代替常量值是常見的方法之一,當(dāng)然如果不想用枚舉的話,使用我們今天所說的主角靜態(tài)工廠方法也是一個(gè)很好的辦法。

插一句:
實(shí)際上,使用枚舉也有一些缺點(diǎn),比如增大了調(diào)用方的成本;如果枚舉類成員增加,會(huì)導(dǎo)致一些需要完備覆蓋所有枚舉的調(diào)用場(chǎng)景出錯(cuò)等。

如果把以上需求用靜態(tài)工廠方法來實(shí)現(xiàn),代碼大致是這樣的:


// Player : Version 2
class Player {
    public static final int TYPE_RUNNER = 1;
    public static final int TYPE_SWIMMER = 2;
    public static final int TYPE_RACER = 3;
    int type;

    private Player(int type) {
        this.type = type;
    }

    public static Player newRunner() {
        return new Player(TYPE_RUNNER);
    }
    public static Player newSwimmer() {
        return new Player(TYPE_SWIMMER);
    }
    public static Player newRacer() {
        return new Player(TYPE_RACER);
    }
}

注意其中的構(gòu)造方法被聲明為了 private,這樣可以防止它被外部調(diào)用,于是調(diào)用方在使用 Player 實(shí)例的時(shí)候,基本上就必須通過 newRunner、newSwimmer、newRacer 這幾個(gè)靜態(tài)工廠方法來創(chuàng)建,調(diào)用方無須知道也無須指定 type 值 —— 這樣就能把 type 的賦值的范圍控制住,防止前面所說的異常值的情況。

插一句:
嚴(yán)謹(jǐn)一些的話,通過反射仍能夠繞過靜態(tài)工廠方法直接調(diào)用構(gòu)造函數(shù),甚至直接修改一個(gè)已創(chuàng)建的 Player 實(shí)例的 type 值,但本文暫時(shí)不討論這種非常規(guī)情況。

3.3 多了一層控制,方便統(tǒng)一修改

我們?cè)陂_發(fā)中一定遇到過很多次這樣的場(chǎng)景:在寫一個(gè)界面時(shí),服務(wù)端的數(shù)據(jù)還沒準(zhǔn)備好,這時(shí)候我們經(jīng)常就需要自己在客戶端編寫一個(gè)測(cè)試的數(shù)據(jù),來進(jìn)行界面的測(cè)試,像這樣:

    // 創(chuàng)建一個(gè)測(cè)試數(shù)據(jù)
    User tester = new User();
    tester.setName("隔壁老張");
    tester.setAge(16);
    tester.setDescription("我住隔壁我姓張!");
    // use tester
    bindUI(tester);
    ……

要寫一連串的測(cè)試代碼,如果需要測(cè)試的界面有多個(gè),那么這一連串的代碼可能還會(huì)被復(fù)制多次到項(xiàng)目的多個(gè)位置。

這種寫法的缺點(diǎn)呢,首先是代碼臃腫、混亂;其次是萬一上線的時(shí)候漏掉了某一處,忘記修改,那就可以說是災(zāi)難了……

但是如果你像我一樣,習(xí)慣了用靜態(tài)工廠方法代替構(gòu)造器的話,則會(huì)很自然地這么寫,先在 User 中定義一個(gè) newTestInstance 方法:

static class User{
    String name ;
    int age ;
    String description;
    public static User newTestInstance() {
        User tester = new User();
        tester.setName("隔壁老張");
        tester.setAge(16);
        tester.setDescription("我住隔壁我姓張!");
        return tester;
    }
}

然后調(diào)用的地方就可以這樣寫了:

    // 創(chuàng)建一個(gè)測(cè)試數(shù)據(jù)
    User tester = User.newTestInstance();
    // use tester
    bindUI(tester);

是不是瞬間就覺得優(yōu)雅了很多?!

而且不只是代碼簡(jiǎn)潔優(yōu)雅,由于所有測(cè)試實(shí)例的創(chuàng)建都是在這一個(gè)地方,所以在需要正式數(shù)據(jù)的時(shí)候,也只需把這個(gè)方法隨意刪除或者修改一下,所有調(diào)用者都會(huì)編譯不通過,徹底杜絕了由于疏忽導(dǎo)致線上還有測(cè)試代碼的情況。

4. 總結(jié)

總體來說,我覺得『考慮使用靜態(tài)工廠方法代替構(gòu)造器』這點(diǎn),除了有名字、可以用子類等這些語法層面上的優(yōu)勢(shì)之外,更多的是在工程學(xué)上的意義,我覺得它實(shí)質(zhì)上的最主要作用是:能夠增大類的提供者對(duì)自己所提供的類的控制力。

作為一個(gè)開發(fā)者,當(dāng)我們作為調(diào)用方,使用別人提供的類時(shí),如果要使用 new 關(guān)鍵字來為其創(chuàng)建一個(gè)類實(shí)例,如果對(duì)類不是特別熟悉,那么一定是要特別慎重的 —— new 實(shí)在是太好用了,以致于它經(jīng)常被濫用,隨時(shí)隨地的 new 是有很大風(fēng)險(xiǎn)的,除了可能導(dǎo)致性能、內(nèi)存方面的問題外,也經(jīng)常會(huì)使得代碼結(jié)構(gòu)變得混亂。

而當(dāng)我們?cè)谧鳛轭惖奶峁┓綍r(shí),無法控制調(diào)用者的具體行為,但是我們可以嘗試使用一些方法來增大自己對(duì)類的控制力,減少調(diào)用方犯錯(cuò)誤的機(jī)會(huì),這也是對(duì)代碼更負(fù)責(zé)的具體體現(xiàn)。

5. 最后

—— 感謝你花費(fèi)了不少時(shí)間看到這里,但愿文章內(nèi)容沒讓你感到虛度。

【 不只Android】

關(guān)于作者

https://github.com/barryhappy
http://www.barryzhang.com

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容