iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 19
0

有一天艾倫正在開發「超級約翰」這款遊戲,這是一個橫向捲軸遊戲,主角可以透過跳躍來踩死敵人,關卡的目標就是一路過關斬將並將最後的旗子升起來。艾倫正在開發最重要的跳躍,只要偵測到搖桿有按下“A”,主角就會跳一下。但是在遊戲結束時會有一段動畫,玩家這時候不能按“A”來跳躍。所以艾倫就寫了以下這段程式碼:

class Game {

    private Status gameStatus;
    private Player player;

    public void onUpdateGameStatus(Status status) {
        this.gameStatus = status;
    }
    
    public void onKeyDown(String key) {
        if (key.equals("A")) {
	        if (gameStatus == Status.OVER) {
	            //Do nothing
	            //TODO 這邊怪怪的
	        } else {
	            player.jump();
	        }
        }
    }
}

艾倫越看越不滿意,所以又想了另一個解法:

class Game {

    private Status gameStatus;
    private Player player;

    public void onUpdateGameStatus(Status status) {
        this.gameStatus = status;
        if (status == Status.OVER) {
            player == null;
        }
    }
    
    public void onKeyDown(String key) {
        if (key.equals("A")) {
            if (player != null) {
                player.jump();
            }
        }
    }
}

這樣還是很奇怪啊!為了不讓角色可以操作而硬給他 null ,而且角色也沒有真的消失了,只是在跑動畫,這樣要是遊戲有劇情發生時是否也要用一樣的技巧?劇情結束了再把 player 塞回去嗎?更不用說未來還有很多不同的情況要處理。好險,他想到了之前看過了一些 Design Pattern,也許有幾個可以派上用場!

如果讀者對物件導向開發有一定的經驗的話,相信你一定碰到不少次的 Null 判斷。也很常為了 NullPointerException 而苦惱著。又或者是學了這麼多 Design Pattern,用了之後發現有些時候還是會有髒髒的 Null 判斷,現實上沒有像大家介紹的一樣那麼美好,別喪氣!Null Object Pattern 可以為你帶來救贖。

Null Object 介紹

Null Object Pattern 會有至少一個 real object,一個 null object,以及他們共通的繼承對象 abstract object,Client 會透過 abstract object 的介面來使用 real object 或是 null object。這種使用方式就是大家所熟悉的多型 (Polymorphism)

Foo

Null Object 範例程式

讓我們繼續艾倫的故事吧!艾倫想到了他可以將控制角色的邏輯放到 PlayerController 這個類別裡面,這樣一來就可以封裝 Player 的控制行為了。

這邊用 Java 來當例子:

interface PlayerController {
   void onKeyDown(String key);
}

class PlayerControllerImpl implements PlayerController {
    private Player player;
    
    public PlayerControllerImpl(Player player) {
        this.player = player;
    }

    public void onKeyDown(String key) {
        if (key.equals("A")) {
            player.jump();
        } else if(key.equals("B")) {
            player.dash();
        }
    }
}

class NullPlayerController implements PlayerController {
    public void onKeyDown(String key) {
       //Do nothing
    }
}

這樣一來,只要在遊戲結束時切換 PlayerController,剩下的就可以不用管了!

class Game {

    private PlayerController playerController;
    private Player player;

    public void onUpdateGameStatus(Status status) {
        if (status == Game.Over) {
            //不管玩家按什麼都不會影響
            playerController = new NullPlayerController();
        } else {
            ...
        }
    }
    
    public void onKeyDown(String key) {
        //隨時都可以委派給 PlayerController,不用在這裡知道遊戲的狀態
        playerController.onKeyDown(key);
    }
}

在艾倫原本的設計中,需要知道目前的遊戲狀態才能決定遊戲角色是否要做相對應的行為,巢狀的 if-else 判斷就因此出現了。未來需求越加越多時,很有可能會再出現另外一層的 if-else 。

另外一個好處是程式碼被妥善的放到他應該關注的地方了。遊戲狀態一改變,就更換了 PlayerController 的實體,所以下一個閱讀這段程式碼的人馬上就會知道遊戲狀態與控制器之間的關係,而不需要藉由多層的 if-else 來去推敲這些複雜的關係。

Null Object 使用時機

  • Null Object Pattern 通常會跟其他 Pattern 一起出現,像上面的例子就是跟 Strategy Pattern 的結合,使用 Strategy Pattern 的目標之一就是要減少散落在各地的條件判斷,當遇到什麼事都不需要做的情況時,Null Object Pattern 就會派上用場了!
  • Null Object Pattern 也通常會是 Flyweight Pattern,因為行為基本上會是一樣的,所以可以重用同一個實例,來達到減少記憶體的使用量。
  • 雖然他的名字是 Null Object,但其實不一定碰到 Null reference 才能使用 Null Object, 像上面的例子就是這樣故意安排的,不做任何事情或是擁有預設行為也是一個使用 Null Object 的好時機!

總結

我很喜歡這個 Pattern,他很優雅了解決空實作的問題,充份的運用了多型所帶來的好處。剛剛也提到了他通常會搭配其他 Pattern 一起出現,所以下次當你遇到一個註解寫著 //Do nothing 的時候就可以想想,是不是還有什麼概念可以再進一步去抽象出來(因為聞到了 Null Object Pattern 的味道),有可能可以變成 Strategy Pattern, State Pattern 或是 Command Pattern,但是也要注意,如果邏輯沒有很複雜的話,也不需要花太多時間在這上面,等到主要問題浮現時再去做因應的設計。今天的介紹就到這邊,感謝大家的收看!

作者:Yanbin


上一篇
[Design Pattern] Memento 備忘錄模式
下一篇
[Design Pattern] Chain of Responsibility 責任鍊模式
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言