重構遺留程式碼(9):分析 Concerns

EluQ發表於2014-11-30

舊程式碼,醜陋的程式碼,複雜的程式碼,義大利麵條似的程式碼,鬼話廢話……就是四個字:遺留程式碼。這是一個系列文章,將有助於你處理並解決它。

在這篇教程中,我們將繼續關注業務邏輯。我們將評估RunnerFunctions.php是否屬於某個類,如果是這樣,屬於哪個類?我們將考慮關注和方法所屬。最後,我們將學點關於模擬的概念。那麼,你還在等什麼?繼續閱讀吧。

 

RunnerFunctions – 從程式導向到物件導向

即使我們大部分程式碼是物件導向的形式,很好地組織類,有些功能只是定義在檔案中。我們需要採取些措施以給予RunnerFunctions.php的函式更加物件導向的樣子。

我的第一本能只是將它們放在一個類中。這沒什麼天才的,但它是使得我們開始修改程式碼。讓我們看看這個想法能否真正實現。

如果我們這麼做,我們需要修改測試程式和GameRunner.php以使用新的類。我們暫且將類稱為通用的,當需要時重新命名它將是容易的。我們甚至不知道這個類是將獨立存在還是將融入Game類。所以還不要擔心命名。

在我們的GoldenMasterTest.php檔案中,我們必須修改執行程式碼的方式。函式是generateOutput(),它的第三行需要被修改為建立一個新物件並呼叫run()。但這樣測試失敗了。

現在我們需要進一步修改新的類。

我們只需要修改run()方法的while表示式條件。新程式碼從當前類通過為didSomebodyWin()和isCurrentAnswerCorrect()準備的$this->來呼叫它們。

這使金牌大師程式通過了,但它破壞了Runner測試。

問題在assertAnswersAreCorrectFor()中,但首先建立一個Runner物件很容易解決。

同樣的問題也需要在三個其他的函式中部署。

雖然這使程式碼通過了,但它引入了一些程式碼重複。因為我們現在所有的測試程式都通過了,可以提取Runner建立到setUp()方法中了。

幹得漂亮。所有這些新的建立和重構讓我思考。我們命名變數runner。也許我們的類也可以以相同名字命名。讓我們重構它。應該是容易的。

如果在上面的框你沒有勾選“Search for text occurrences”,不要忘了手動修改你包括的範圍,因為重構也會重新命名檔案。

現在我們有了一個檔案命名為GameRunner.php,另一個命名為Runner.php,還有第三個命名為Game.php。我不知道對你怎麼樣,但這似乎讓我很困惑。如果這是在我生命中第一次見到這三個檔案,我會不知道哪個是做什麼的。我們需要至少移除它們中的一個。

在我們重構的早期階段,建立RunnerFunctions.php的原因是構建一種為測試包含所有方法和檔案的方式。我們需要進入任何程式碼,但不是執行所有程式碼,除非在金牌大師程式中的已準備的環境中。我們仍可以做相同的事,只是不從GameRunner.php執行程式碼。在我們繼續之前,需要更新包含和在程式碼裡建立新的類。

這將做到這點。我們需要明確包含Display.php,以便當Runner試圖建立新的CLIDisplay的時候,它會知道去實現什麼。

 

分析關注

我認為物件導向程式設計最重要的特性之一是分析關注。我常常問我自己的問題是,“這個類做了它的名字所表達的意思嗎?”,“這個方法關注的是這個物件嗎?”,“我的物件應該關心具體值嗎?”。

令人驚異的是,這些型別的問題在澄清業務域和軟體架構上有著巨大的作用。在Syneto,我們在小組中問答這些型別的問題。許多時候當一個開發者有了困境,他或者她只要站起來,從團隊中請求2分鐘的關注來了解我們對於一個課題的意見。那些對程式碼架構熟悉的人會從軟體的角度回答,而另一些對業務領域更熟悉的人可能揭示業務方面一些重要的見解。

讓我們試著考慮下我們的例子中的關注。我們可以繼續專注於Runner類。比起Game類,這個類非常有可能消除或者改造。

首先,Runner應該關心isCurrentAnswerCorrect()怎麼運作的嗎?Runner應該有任何關於問題和答案的知識嗎?

看起來似乎這個方法從Game類消除更好點。我強烈認為關於問答的Game類應該關心答案正確與否。我真的相信Game類必須關注為當前問題提供答案的結果。

是時候行動了。我們將做移動方法重構。正如從我之前的教程中我們已經看到的,我將只給你看最終結果。

有必要注意到不只是方法沒了,而且用來定義答案限制的常量也沒了。

但didSomebodyWin()怎麼樣?Runner應該決定何時某人勝出嗎?如果我們看看方法體,我們可以看到一個問題如黑夜中的手電筒一樣突出。

無論這個方法做了什麼,它只作用在Game類物件上。它驗證了從Game返回的當前答案。之後它返回了無論一個game物件在它的wasCorrectlyAnswered()或wrongAnswer()方法中返回的值。這個方法有效地沒執行任何操作。它所關注的全部是Game類。這是一個經典的程式碼示例,稱為依戀情。一個類做了另一個類應該做的事。是時候移動它了。

像往常一樣,我們首先移動測試程式。測試驅動開發?任何人?

這讓我們沒有更多的測試可以執行,所以這個檔案現在可以移走了。刪除是程式設計中我最喜歡的部分。

當我們執行測試程式時,我們得到一個很漂亮的錯誤:

也是時候修改程式碼了。把方法複製並粘帖進Game類將神奇般得使所有測試通過。包括了舊的方法和移入GameTest類的方法。但將方法放在合適的位置的同時帶來了兩個問題:Runner也需要修改,我們傳入的假Game物件因為它是Game類的一部分而並不需要做任何事。

修正Runner是很簡單的。我們只需將$this->didSomebodyWin(…)修改為$aGame->didSomebodyWin(…)。在我們的下一步之後,我們將再次回到這裡並修改它,測試重構。

是時候做些模擬了!取代使用定義在我們測試程式最後的假的類,我們將使用Mockery。它允許我們簡單得覆寫Game中的方法,希望它被呼叫並且返回我們想要的值。當然,我們可以通過假的類繼承Game類並重寫我們自己的方法做到這點。但工具存在了,為什麼做這樣的工作?

在我們第二個方法重寫之後,我們可以擺脫假的Game類和任何初始化它的方法了。問題解決了!

 

最後的思考

即使我們能想到的只有Runner,但今天我們取得了很大的進步。我們學習了職責,我們定義了屬於另一個類的方法和變數。我們在更高層次上思考並且我們走向了一個更好的解決方案。在Syneto團隊,有一種強烈的信仰,即有辦法把程式碼寫好,並且除非使程式碼至少更清晰一點否則從不提交修改。

這是在經過一段時間之後,能夠帶來好得多的程式碼庫,以更少的相關性,更多地測試並最終更少的錯誤的技術。

感謝,佔用你的時間了。

相關文章