用Java把大象放到冰箱裡

weixin_34185364發表於2017-01-03

『把大象放到冰箱裡,需要哪三步?』——這是源於春晚小品的一個段子。

如果我們用程式語言Java來表達這個過程,那麼大概是:

    openFridgeDoor();
    putElephantIntoFridge(elephant);
    closeFridgeDoor();

如果寫到這裡就結束,那麼本文不過是一個惡作劇罷了。

Are you kidding me?

而實際上,本文比你預想中的要嚴肅認真得多。

構思過程

假設,真的有這麼一個把大象放到冰箱裡的需求,並且有可程式設計的機器人,可以代為實現物理操作,那麼,我們該如何設計程式碼呢?

冰箱的檢查

按照原來的框架,第一步和第三步都是非常簡單的。我們假定,用Robot這個類的方法呼叫,來代表機器人操作。開關冰箱的操作可以表達為:

    private void openFridgeDoor() {
        Robot.openFridgeDoor(this.fridge);
    }

    private void closeFridgeDoor() {
        Robot.closeFridgeDoor(this.fridge);
    }

第二步操作比較複雜,需要細化一下。我首先想到的問題是冰箱。

成年大象的體積,比常見冰箱要大得多,這是一個難點。對此,程式上要做判斷與處理。

    private void putElephantIntoFridge(Elephant elephant) {
        if (elephant.size() > this.fridge.size()) {
            findABiggerFridge(elephant.size());
        }
        Robot.putElephantIntoFridge(elephant, this.fridge);
    }

但是,這樣就衍生了兩個問題。如果大象太大,或者指定大小的大冰箱真的找不到,那該怎麼辦?

    private void findABiggerFridge(long size) throws FridgeNotFoundException {
        Fridge newFridge = Robot.findABiggerFridge(size);
        if (newFridge != null) {
            this.fridge = newFridge;
        } else {
            throw new FridgeNotFoundException(size);
        }
    }

此時,我們最不願意見到的事情發生了。我們處理不了這個異常,程式碼需要重新調整。

此外,說到異常,Robot.putElephantIntoFridge似乎也可能拋一個異常:ElephantDefeatRobotException

調整程式碼結構

不僅僅是大冰箱找不到的問題。即使找到了,在更換冰箱後,原冰箱的門沒有關,新冰箱的門也沒有開啟。說到底,為什麼要開啟冰箱門才能發現不夠大?

另外,也有命名問題。程式設計時,冠詞a、an、the不應該出現;openFridgeDoor也顯得冗餘,在這個語境中,沒有人會認為openFridge是開啟冰箱的電源或後蓋吧?

因此,最上層應改為:

    public void putElephantIntoFridge(Elephant elephant) throws FridgeNotFoundException {
        Fridge fridge = null;
        try {
            fridge = openFridge(elephant.size());
            fridge.putElephant(elephant);
        } finally {
            if (fridge != null) {
                fridge.close();
            }
        }
    }

其中,openFridge裡應該包含發現大冰箱與開啟冰箱門兩個操作,以及發現不了就丟FridgeNotFoundException的情況。

在這次調整中,我們把具體操作冰箱的Method都封裝到冰箱這個類中。並且,用try-catch-finally來保證冰箱門的開關匹配。

不過,你可能已經發現了,我們還是沒有處理異常。

其它問題

到了我們負責實現的最頂層,我們仍然無法找到『找不到大冰箱』、『大象幹掉了機器人』這兩個異常的處理辦法。

其實,這確實不該由我們來處理,而且不能用catch就這麼吞掉,不然上層還以為大象已經成功放到冰箱裡去了。因此,異常應該傳遞到上層。

還有一個細節問題,大象到底能不能殺?

如果大象能殺,呃……這雖然有些殘忍,並且可能觸犯了法律,但是size這個問題就好解決了。我們可以宰了大象,這樣體積就可以減小。如果還是不行,還可以把肉剁碎,壓縮一下嘛。

這種情況下,上面的程式碼又得調整。因為,每個大象有三個size,一個是活著的大象需要的空間大小,一個是大象的肉的總體積,還有一個是壓縮後的最小體積。

而且,你可能已經發現了,我為了簡化問題,用的是一個long型別的大小,而非複雜的長寬高。

如果不能殺……說到底,為什麼要把大象放到冰箱裡?

活活凍死?這好像更殘忍。

完整結果

ElephantHandler類,負責提供給外界呼叫,專門處理『把大象放到冰箱裡』這件事。

public final class ElephantHandler {
    private Robot robot;

    public ElephantHandler(Robot robot) {
        this.robot = robot;
    }

    public void putElephantIntoFridge(Elephant elephant) throws
            FridgeNotFoundException, ElephantDefeatRobotException {
        try (Fridge fridge = openFridge(elephant.size)) {
            fridge.put(elephant);
        }
    }

    private Fridge openFridge(Size size) throws FridgeNotFoundException {
        Fridge fridge = this.robot.findBiggerFridge(size);
        fridge.open();
        return fridge;
    }
}

上面的程式碼又做出了一些改進。

  • 用Robot的例項,而非類。
  • 冰箱的開關,用Java 1.7的try-with-resource特性來控制。
  • 找冰箱的操作,完全委託給機器人。
  • putElephant改成put,語意更簡潔,在當前情況下也不會混淆。

下面是Fridge類。

final class Fridge implements AutoCloseable {
    private final Robot robot;

    Fridge(Robot robot) {
        this.robot = robot;
    }

    @Override
    public void close() {
        this.robot.closeFridge(this);
    }

    void open() {
        this.robot.openFridge(this);
    }

    void put(Elephant elephant) throws ElephantTooBigException {
        this.robot.putElephantIntoFridge(elephant, this);
    }
}

還有FridgeNotFoundException等幾個異常類,行文從簡,略。

為什麼我在哪裡都沒有處理這個ElephantTooBigException?因為不知道怎麼處理。

到這裡,你必然已經發現了,重要的操作都在Robot裡,而我卻沒有給出Robot這個類的程式碼。

這個嘛……就不要糾結了,難道我真的要把大象宰給你看?

意義

應該沒有人會真的認為,我寫這篇文章是真的想介紹怎麼把大象放到冰箱裡吧?

我想以此為例,談談程式碼的層次、專案的模組、以及錯誤的架構。

程式碼的層次

在這裡,有三層程式碼。

上層,傳入Elephant、呼叫ElephantHandler.putElephantIntoFridge的模組;
中層,就是我們實現的部分,做一些業務邏輯的處理;
下層,負責幹實事的Robot。

實際的程式設計,往往都發生在中層。

這樣的分工是必要的。每一個實際的專案,都會逐漸變得複雜。唯有模組分明,才能更好地分工協作,最終完成。

本文展示的程式碼,集中精力解決『把大象放到冰箱裡』的步驟,梳理了合適的流程,處理了冰箱、大象、機器人之間的關係,並且給出了可能的異常狀況。

程式碼的責任鏈

既然是玩物件導向程式設計,你對責任鏈模式應該不會陌生。實際上,異常系統就是一個責任鏈模式。

異常是必須要被處理的,問題是誰來處理。

ElephantTooBigException是我需要處理的異常,然而,如你所見,我沒有處理。因為,我已經作了相應的流程控制,確保這個異常不會發生。我寫的這兩個類,主要目的就是這個。如果真的發生了,那麼毫無疑問是我的問題。但我不應該增加catch,而是要去檢查流程與邏輯,確保這個異常不會發生。如果我為了確保萬無一失,增加catch,這只是自欺欺人,讓問題發生時更難找到原因。

Robot也是有一些其它異常的,比如冰箱門打不開,或者關不上。但這是它自身必須確保實現的功能問題,應該由Robot的開發者來解決。所以,我的程式碼裡就根本不考慮這兩個操作可能出問題。如果真的出問題,bug應該丟給Robot的開發者,與我無關。

ElephantDefeatRobotException是上層應該處理的異常,畢竟,Robot是上層傳遞給我的。Robot被幹掉了,應該由上層來換一個更強的Robot;如果上層的Robot都被(五殺暴走的大象)幹掉了,那麼也該是上層向它的上層拋異常,也與我無關。

FridgeNotFoundException這個異常,恐怕上層也無法解決。這有兩種可能:一是Robot的問題,明明有夠大的冰箱,它卻找不到,這情況類似於冰箱門打不開;二是終端使用者的問題,市面上根本沒有能裝下大象的冰箱,你下的這個命令是什麼意思?總之,還是與我無關。

(瞧我這精湛的甩鍋功力,只問你服不服?)

專案的模組

如果我只是在談程式設計師該如何設計程式碼的層次結構,那麼未免太小。實際上,經驗豐富的程式設計師不需要我來提點,而菜鳥們更應該去看《重構》、《完美程式碼》、《程式碼整潔之道》之類的大書,看散文沒什麼用。

我真正想談的是專案管理。

前面說了多次『與我無關』,建議在看本文的一執行緒序員,切勿模仿!

因為,無論是懂技術的開發Leader,還是不懂技術的大小Boss,都不喜歡聽到這句話。他們更希望聽到的是,這個問題與誰有關,最希望聽到的是,這個問題怎麼解決。『與我無關』,這句話只是把鍋甩在地上,讓鍋沒有人背,讓問題不能及時解決。人人都說『與我無關』,那麼問題由誰來解決?雖然他們是錯的,但是人在屋簷下、不得不低頭,職場中人還是要懂得明哲保身、趨利避害才是,以後不要這麼說了。

剛才我好像說到『他們是錯的』。既然說漏了嘴,那就說完好了。

在模組分明的專案中,每個人都獨立地負責一個或幾個模組。要證明『問題不是出在我的模組』,是很簡單的,而要證明一定是別人負責的某個模組的問題,卻比較難。如果能做到這一步,那麼問題基本已經定位清楚了,要解決也不是難事,而時間的開銷卻不小。

在現在常見的處理模型中,更多的是讓先遇到bug的模組負責分析。如果不是他的問題,讓他就找到出問題的模組,並且轉過去。在正常情況下,這樣也是比較高效的。然而,不正常情況雖然數量少,卻會佔用大多數時間。讓我們在工作中花費大量時間的,往往不是最擅長的本職工作,而是一些不熟悉不擅長的狀況。

想想Java的異常系統,會發現這是更加簡單有效的。在Java的每層呼叫棧中,遇到下面拋上來的異常,只有兩個選擇:該處理就catch住,不該處理就往上層拋。所以,只要證明『與我無關』,就夠了。

而現實問題是,當代的issue處理系統,比如JIRA,其模組分工表是毫無聯絡的。既沒有規定誰才能轉問題給我們,也沒有規定我們只能把問題轉給誰。N個模組之間,是N×(N-1)/2的關係。所以,如果只證明『與我無關』,就相當於把問題推給了其它N-1個模組,而它們大多是與問題完全無關的。而且,更常見的是模組劃分不夠細,眉毛鬍子一把抓的情況。

於是,『與我無關』成了禁語。我們不得不承擔那些不屬於我們的責任,只因為沒有一個清晰的責任鏈。

錯誤的架構

在解決『把大象放到冰箱裡』這個問題時,我們本來只是想細化一下放進去的操作,結果卻完全摒棄了預定的三步法,還向呼叫方丟擲了不止一個異常。

在實際工作中,我們就沒那麼好運了。我們面臨的情況是,架構不能改,異常不能拋,一切問題自行解決。美其名曰:執行力。

這個小品裡的這一段,之所以惹人發笑,不是因為『把大象放到冰箱裡』這麼複雜的問題,被白雲大媽(宋丹丹飾)簡單解決;而是因為這位見識淺薄、好大喜功的白雲大媽,抖了抖機靈,給了個看似玄妙的辦法,自以為解決了問題,其實完全不可行。

小品雖然可樂,而現實卻很可悲。作為一線的執行者,我們有時只能為這種農婦式的高屋建瓴,加班加點地添磚加瓦。

程式碼與程式碼的關係,還是比較簡單的。而人與人、團隊與團隊的關係,就複雜多了。有一些組織架構,註定低效,卻無法可改。

執行公司的既定戰略時,如果有一個底層員工發現了一個不可執行的關鍵異常,能否跨越七八層傳遞到CEO那裡,最終改變原定計劃?程式碼是可以的,人卻往往不行。

結語

最終,我們還是不能把大象放到冰箱裡,因為市面上還買不到能幹掉大象的可程式設計機器人。

Yes, I am kidding. _

相關文章