天天看點

Effective Java——靜态工廠方法1. 序:什麼是靜态工廠方法2. Effective Java3. 除此之外4. 總結5. 最後

1. 序:什麼是靜态工廠方法

在 Java 中,獲得一個類執行個體最簡單的方法就是使用

new

關鍵字,通過構造函數來實作對象的建立。

就像這樣:

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

不過在實際的開發中,我們經常還會見到另外一種擷取類執行個體的方法:

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

↑ 像這樣的:不通過

new

,而是用一個靜态方法來對外提供自身執行個體的方法,即為我們所說的

靜态工廠方法(Static factory method)

知識點:

new

究竟做了什麼?

簡單來說:當我們使用 new 來構造一個新的類執行個體時,其實是告訴了 JVM 我需要一個新的執行個體。JVM 就會自動在記憶體中開辟一片空間,然後調用構造函數來初始化成員變量,最終把引用傳回給調用方。

2. Effective Java

在關于 Java 中書籍中,《Effective Java》絕對是最負盛名幾本的之一,在此書中,作者總結了幾十條改善 Java 程式設計的金玉良言。其中開篇第一條就是『考慮使用靜态工廠方法代替構造器』,關于其原因,作者總結了 4 條(第二版),我們先來逐個看一下。

2.1 靜态工廠方法與構造器不同的第一優勢在于,它們有名字

由于語言的特性,Java 的構造函數都是跟類名一樣的。這導緻的一個問題是構造函數的名稱不夠靈活,經常不能準确地描述傳回值,在有多個重載的構造函數時尤甚,如果參數類型、數目又比較相似的話,那更是很容易出錯。

比如,如下的一段代碼 :

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 類有很多重載函數,對于開發者來說,假如不是特别熟悉的話,恐怕是需要猶豫一下,才能找到合适的構造函數的。而對于其他的代碼閱讀者來說,估計更是需要檢視文檔,才能明白每個參數的含義了。

(當然,Date 類在目前的 Java 版本中,隻保留了一個無參和一個有參的構造函數,其他的都已經标記為 @Deprecated 了)

而如果使用靜态工廠方法,就可以給方法起更多有意義的名字,比如前面的

valueOf

newInstance

getInstance

等,對于代碼的編寫和閱讀都能夠更清晰。

2.2 第二個優勢,不用每次被調用時都建立新對象

這個很容易了解了,有時候外部調用者隻需要拿到一個執行個體,而不關心是否是新的執行個體;又或者我們想對外提供一個單例時 —— 如果使用工廠方法,就可以很容易的在内部控制,防止建立不必要的對象,減少開銷。

在實際的場景中,單例的寫法也大都是用靜态工廠方法來實作的。

如果你想對單例有更多了解,可以看一下這裡:☞《Hi,我們再來聊一聊Java的單例吧》

2.3 第三個優勢,可以傳回原傳回類型的子類

這條不用多說,設計模式中的基本的原則之一——『裡氏替換』原則,就是說子類應該能替換父類。

顯然,構造方法隻能傳回确切的自身類型,而靜态工廠方法則能夠更加靈活,可以根據需要友善地傳回任何它的子類型的執行個體。

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

比如上面這段代碼,Person 類的靜态工廠方法可以傳回 Person 的執行個體,也可以根據需要傳回它的子類 Player 或者 Cooker。(當然,這隻是為了示範,在實際的項目中,一個類是不應該依賴于它的子類的。但如果這裡的 getInstance () 方法位于其他的類中,就更具有的實際操作意義了)

2.4 第四個優勢,在建立帶泛型的執行個體時,能使代碼變得簡潔

這條主要是針對帶泛型類的繁瑣聲明而說的,需要重複書寫兩次泛型參數:

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

不過自從 java7 開始,這種方式已經被優化過了 —— 對于一個已知類型的變量進行指派時,由于泛型參數是可以被推導出,是以可以在建立執行個體時省略掉泛型參數。

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

是以這個問題實際上已經不存在了。

3. 除此之外

以上是《Effective Java》中總結的幾條應該使用靜态工廠方法代替構造器的原因,如果你看過之後仍然猶豫不決,那麼我覺得可以再給你更多一些理由 —— 我個人在項目中是大量使用靜态工廠方法的,從我的實際經驗來世,除了上面總結的幾條之外,靜态工廠方法實際上還有更多的優勢。

3.1 可以有多個參數相同但名稱不同的工廠方法

構造函數雖然也可以有多個,但是由于函數名已經被固定,是以就要求參數必須有差異時(類型、數量或者順序)才能夠重載了。

舉例來說:

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 兩個屬性,如代碼所示,它已經有了兩個構造函數:Child(int age, int weight) 和 Child(int age),這時候如果我們想再添加一個指定 wegiht 但不關心 age 的構造函數,一般是這樣:

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

↑ 但要把這個構造函數添加到 Child 類中,我們都知道是行不通的,因為 java 的函數簽名是忽略參數名稱的,是以

Child(int age)

Child(int weight)

會沖突。

這時候,靜态工廠方法就可以登場了。

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;
    }
}
           

其中的

newChildWithWeight

newChildWithAge

,就是兩個參數類型相同的的方法,但是作用不同,如此,就能夠滿足上面所說的類似

Child(int age)

Child(int weight)

同時存在的需求。

(另外,這兩個函數名字也是自描述的,相對于一成不變的構造函數更能表達自身的含義,這也是上面所說的第一條優勢 —— 『它們有名字』)

3.2 可以減少對外暴露的屬性

軟體開發中有一條很重要的經驗:對外暴露的屬性越多,調用者就越容易出錯。是以對于類的提供者,一般來說,應該努力減少對外暴露屬性,進而降低調用者出錯的機會。

考慮一下有如下一個 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 對外提供了一個構造方法,讓使用者傳入一個 type 來表示類型。那麼這個類期望的調用方式就是這樣的:

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

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

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

提供者期望的構造函數傳入的值是事先定義好的幾個常量之一,但如果不是,就很容易導緻程式錯誤。

—— 要避免這種錯誤,使用枚舉來代替常量值是常見的方法之一,當然如果不想用枚舉的話,使用我們今天所說的主角

靜态工廠方法

也是一個很好的辦法。

插一句:

實際上,使用枚舉也有一些缺點,比如增大了調用方的成本;如果枚舉類成員增加,會導緻一些需要完備覆寫所有枚舉的調用場景出錯等。

如果把以上需求用靜态工廠方法來實作,代碼大緻是這樣的:

// 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);
    }
}
           

注意其中的構造方法被聲明為了

private

,這樣可以防止它被外部調用,于是調用方在使用 Player 執行個體的時候,基本上就必須通過 newRunner、newSwimmer、newRacer 這幾個靜态工廠方法來建立,調用方無須知道也無須指定 type 值 —— 這樣就能把 type 的指派的範圍控制住,防止前面所說的異常值的情況。

插一句:

嚴謹一些的話,通過反射仍能夠繞過靜态工廠方法直接調用構造函數,甚至直接修改一個已建立的 Player 執行個體的 type 值,但本文暫時不讨論這種非正常情況。

3.3 多了一層控制,友善統一修改

我們在開發中一定遇到過很多次這樣的場景:在寫一個界面時,服務端的資料還沒準備好,這時候我們經常就需要自己在用戶端編寫一個測試的資料,來進行界面的測試,像這樣:

// 建立一個測試資料
    User tester = new User();
    tester.setName("隔壁老張");
    tester.setAge(16);
    tester.setDescription("我住隔壁我姓張!");
    // use tester
    bindUI(tester);
    ……
           

要寫一連串的測試代碼,如果需要測試的界面有多個,那麼這一連串的代碼可能還會被複制多次到項目的多個位置。

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

但是如果你像我一樣,習慣了用靜态工廠方法代替構造器的話,則會很自然地這麼寫,先在 User 中定義一個 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;
    }
}
           

然後調用的地方就可以這樣寫了:

// 建立一個測試資料
    User tester = User.newTestInstance();
    // use tester
    bindUI(tester);
           

是不是瞬間就覺得優雅了很多?!

而且不隻是代碼簡潔優雅,由于所有測試執行個體的建立都是在這一個地方,是以在需要正式資料的時候,也隻需把這個方法随意删除或者修改一下,所有調用者都會編譯不通過,徹底杜絕了由于疏忽導緻線上還有測試代碼的情況。

4. 總結

總體來說,我覺得『考慮使用靜态工廠方法代替構造器』這點,除了有名字、可以用子類等這些文法層面上的優勢之外,更多的是在工程學上的意義,我覺得它實質上的最主要作用是:能夠增大類的提供者對自己所提供的類的控制力。

作為一個開發者,當我們作為調用方,使用别人提供的類時,如果要使用 new 關鍵字來為其建立一個類執行個體,如果對類不是特别熟悉,那麼一定是要特别慎重的 ——

new

實在是太好用了,以緻于它經常被濫用,随時随地的 new 是有很大風險的,除了可能導緻性能、記憶體方面的問題外,也經常會使得代碼結構變得混亂。

而當我們在作為類的提供方時,無法控制調用者的具體行為,但是我們可以嘗試使用一些方法來增大自己對類的控制力,減少調用方犯錯誤的機會,這也是對代碼更負責的具體展現。

5. 最後

—— 感謝你花費了不少時間看到這裡,但願文章内容沒讓你感到虛度。