被遺忘的設計模式——空對象模式(Null Object Pattern)

一、Pattern name

Provide an object as a surrogate for the lack of an object of a given
type. The Null Object provides intelligent do nothing behavior, hiding
the details from its collaborators.

二、Problem

任何沒有實際應用場景的設計模式,都是在耍流氓。學習設計模式,不僅僅是爲了領悟其精髓,更爲了在實踐設計當中去運用,去變通,下面我們來看看,什麼情況下,這個Null Object Pattern會派上用場呢?

假設這樣一個場景:

在一個圖書信息查詢系統中,你調用一個方法,傳過去你要查找圖書的ID,然後它返回給你,你要查找的圖書對象,這樣你就可以調用對象的方法來輸出圖書的信息。

我想這種場景在程序設計中還是比較常見的。下面,我們來實現以下具體的代碼。

首先,我們來看一下ConcreteBook類的代碼(提供構造函數和展示圖書信息的show()方法。):

public class ConcreteBook {
    private int ID;
    private String name;
    private String author;

    // 構造函數
    public ConcreteBook(int ID, String name, String author) {
        this.ID = ID;
        this.name = name;
        this.author = author;
    }

    /**
     * 
     * Description About show: <br>
     * 展示圖書的相關信息
     * 
     * @version V1.0
     */
    public void show() {
        System.out.println(ID + "**" + name + "**" + author);
    }

}

我們再來看看創建圖書對象的圖書工廠的代碼(主要提供一個獲得ConcreteBook的方法):

public class BookFactory {
    /**
     * 
     * Description About getBook: <br>
     * 根據ConcreteBook的ID,獲取圖書對象。
     * @param ID 圖書的ID
     * @return 圖書對象
     * @version V1.0
     */

    public ConcreteBook getBook(int ID) {
        ConcreteBook book = null;
        switch (ID) {
        case 1:
            book = new ConcreteBook(ID, "設計模式", "GoF");
            break;
        case 2:
            book = new ConcreteBook(ID, "被遺忘的設計模式", "Null Object Pattern");
            break;
        default:
            book = null;// 其實這個可以省略,因爲初始化已經賦值爲null。
            break;
        }

        return book;
    }
}

最後,來看一下客戶端的代碼:

public class Client {

    static void main(String[] args) {
        BookFactory bookFactory = new BookFactory();
        ConcreteBook book = bookFactory.getBook(1);
        book.show();
    }

}

上面三段代碼很簡單,我就不做詳細解釋了。下面,我們來運行一下,結果如下:
在這裏插入圖片描述
很好,運行很順利,這時,我們把ConcreteBook book = bookFactory.getBook(1);中的1改爲2,恩,也運行成功。這時候,我們改成-1。再來運行一下,發現如下報錯:
在這裏插入圖片描述
空指針報錯,是的,這應該是Java初學者見到最多的報錯了。它提示我們第28行book.show()報錯。這是爲什麼呢?因爲我們通過bookFactory.getBook()方法獲取ConcreteBook對象的時候,如果我們傳入的參數,即圖書的ID,屬於非法值(如-1)或者不存在(如3)的話(其實這種情況是經常遇到的。),就會返回null,表示我們查找的圖書信息並不存在。這時,book爲null.你再調用book.show()。當然要報空指針的錯誤了。那怎麼解決呢?

我們比較常規的做法就是在客戶端加一個判斷,判斷是否爲null。如果爲null的話,就不再調用show()方法。如果不爲null再調用show()方法。更改如下:

public static void main(String[] args) {
        BookFactory bookFactory = new BookFactory();
        ConcreteBook book = bookFactory.getBook(-1);
        //判斷book對象是否爲null。
        if (book == null) {
            System.out.println("book對象爲 null。");
        } else {
            book.show();
        }
    }

此時,再運行,就不會報錯了。而是,輸出了:book對象爲null。

但是,你有沒有考慮過?這樣做,確實消除了報錯,但是這樣做真的好嗎?你想如果在一段程序中有很多處調用getBook()方法或者有很多個客戶端的話(比如圖書館的查詢終端肯定不止一個啊),豈不是很多處都要判斷book對象是否爲null?這還不算壞,如果哪一處沒有判斷,然後報錯了,很有可能導致程序沒法繼續運行甚至崩潰。而且,你要記住,永遠都不要太相信客戶端(Client),不要把整個程序的穩定性寄託在客戶端身上。還有,像上面的處理方法,當獲取對象爲null的時候,輸出的提示信息是有客戶端來定製的,這樣豈不是把主動權交給了客戶端,而不是我們系統本身?

那究竟應該如何實現纔會更加合適呢?那就要用到我們今天要講的Null Object Pattern——一種被遺忘的設計模式

三、Solution

首先,我們來看一下Null Object Pattern的UML類圖結構:
在這裏插入圖片描述
這個類圖結構其實還是很簡單的,這裏面的RealObject其實就相當於我們的ConcreteBook類,而NullObject就是我們將要增加的空對象類,而AbstractObject類就是我們要提出來的父類。我們只是在Client和AbstractObject之間增加了一個BookFactory而已。

下面,我們來改一下我們的代碼:

新增的抽象接口Book類的代碼:

interface Book {
    // 判斷Book對象是否爲空對象(Null Object)
    public boolean isNull();

    // 展示Book對象的信息內容。
    public void show();
}

新增的空對象類NullBook類的代碼(繼承Book類):

public class NullBook implements Book {
    public boolean isNull() {
        return true;
    }

    public void show() {

    }
}

原有的ConcreteBook類修改後的代碼(增加對Book接口的實現,實現isNull方法):

public class ConcreteBook implements Book{
    private int ID;
    private String name;
    private String author;

    // 構造函數
    public ConcreteBook(int ID, String name, String author) {
        this.ID = ID;
        this.name = name;
        this.author = author;
    }

    /**
     * 
     * Description About show: <br>
     * 展示圖書的相關信息
     * 
     * @version V1.0
     */
    public void show() {
        System.out.println(ID + "**" + name + "**" + author);
    }
    public boolean isNull(){
        return false;
    }
}

工廠類(BookFactory)修改後的代碼(返回對象從ConcreteBook改爲Book,並當ID屬於非法值或者不存在時,返回NullBook對象。):

public class BookFactory {
    /**
     * Description About getBook: <br>
     * 根據ConcreteBook的ID,獲取圖書對象。
     * @param ID 圖書的ID
     * @return 圖書對象
     * @version V1.0
     */

    public Book getBook(int ID) {
        Book book;//將原來的ConcreteBook改爲Book
        switch (ID) {
        case 1:
            book = new ConcreteBook(ID, "設計模式", "GoF");
            break;
        case 2:
            book = new ConcreteBook(ID, "被遺忘的設計模式", "Null Object Pattern");
            break;
        default:
            book = new NullBook();//創建一個NullBook對象
            break;
        }

        return book;
    }
}

客戶端的代碼爲:

public static void main(String[] args) {
        BookFactory bookFactory = new BookFactory();
        Book book = bookFactory.getBook(-1);
        book.show();
    }

運行一下,我們發現,即使傳入的參數是非法值或者不存在的值時,也不會報錯了,這是Null Object Pattern的第一個好處。但是現在不報錯,也沒有任何輸出,肯定不夠友好,不夠人性化。此時,在NullBook類的show方法中,我們可以定製我們的輸出提醒,當用戶調用空對象的show方法時,就會輸出我們定製的提醒。這回我們可以實現,一處定製,處處輸出,主動權在我們手裏,而不是在客戶端的手裏。這是Null Object Pattern的第二個好處。

比如我們進行如下修改,修改後的NullBook類代碼:

public class NullBook implements Book {
    public boolean isNull() {
        return true;
    }

    public void show() {
        System.out.println("Sorry,未找到符合您輸入的ID的圖書信息,請確認您輸入的不是非法值。");
    }
}

此時,在執行一下Client,你會發現控制檯輸出爲:Sorry,未找到符合您輸入的ID的圖書信息,請確認您輸入的不是非法值。

其實,雖然在客戶端我們不進行檢測也可以保證程序不報錯,但是最好的方式,還是進行相應的檢測,如下:

 public static void main(String[] args) {
        BookFactory bookFactory = new BookFactory();
        Book book = bookFactory.getBook(-1);
        if (book.isNull()) {
            //這裏由客戶端定製提醒代碼
            System.out.println("兄弟,你輸入的ID不符合規範吧。");
        }else{
            book.show();
        }
    }
複製代碼

我們看到相比之下,book.isNull()比book == null更加優雅一點。到這裏,Null Object Pattern大概就介紹完了。我們可以看到,其實Null Object Pattern還是有點意思的,可以說使整個系統更加堅固了。

四、Consequences

Null Object Pattern,作爲一種被遺忘的設計模式,卻有着不能被遺忘的作用。

(1)它可以加強系統的穩固性,能有有效地防止空指針報錯對整個系統的影響,使系統更加穩定。
(2)它能夠實現對空對象情況的定製化的控制,能夠掌握處理空對象的主動權。
(3)它並不依靠Client來保證整個系統的穩定運行。
(4)它通過isNull對==null的替換,顯得更加優雅,更加易懂。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章