Clean Code & Unit Test

本文部分內(nèi)容及示例來(lái)自Google Testing Blog对粪,有興趣可以在文末點(diǎn)擊鏈接查看原文。

Preface

本篇的內(nèi)容是單元測(cè)試鹃共,看標(biāo)題可能會(huì)奇怪给涕,為什么是Clean Code在前面豺憔,因?yàn)槲乙f(shuō)的并不是怎么寫(xiě)單元測(cè)試额获,寫(xiě)單元測(cè)試的方法在網(wǎng)上隨便一搜就有很多,并且因?yàn)楦鞣N框架的關(guān)系恭应,寫(xiě)起來(lái)非常簡(jiǎn)單抄邀,可以說(shuō)基本上是沒(méi)有任何難度,但是可能你看了很多文檔或者教程之后昼榛,覺(jué)得單元測(cè)試很簡(jiǎn)單了境肾,然后打開(kāi)項(xiàng)目準(zhǔn)備寫(xiě),卻發(fā)現(xiàn)不知道從何下手褒纲,或者寫(xiě)出一下看起來(lái)像是單元測(cè)試,其實(shí)卻不太一樣的代碼钥飞,或者覺(jué)得很難就放棄了莺掠。然而單元測(cè)試其實(shí)非常簡(jiǎn)單,事實(shí)上這正是本篇內(nèi)容要解決的問(wèn)題读宙,如何寫(xiě)出可測(cè)試的代碼彻秆,雖說(shuō)是為了測(cè)試,實(shí)質(zhì)上即是clean code结闸,注意這里clean是動(dòng)詞唇兑。

那我們先說(shuō)clean code,什么叫clean code桦锄,簡(jiǎn)單理解就是如何寫(xiě)出整潔的代碼扎附,或者說(shuō)如何寫(xiě)出高質(zhì)量的代碼,好代碼结耀。那么留夜,首先說(shuō)什么樣的代碼是好的代碼?

  1. 易于閱讀

  2. 易于修改

  3. 沒(méi)有bug

  4. 魯棒性

  5. ...

上面只是舉一些例子图甜,實(shí)際上不只這些優(yōu)點(diǎn)碍粥。

先看一張經(jīng)典圖片:

wtfm.jpg

我所接觸到的大部分項(xiàng)目,很明顯是右邊黑毅。
那么如何解決這個(gè)問(wèn)題呢嚼摩?
答案是不斷的學(xué)習(xí)和實(shí)踐。寫(xiě)出高質(zhì)量代碼是相當(dāng)困難的事情矿瘦,首先要學(xué)習(xí)各種理論知識(shí)枕面,但是這就如同騎自行車(chē),哪怕給你講的再好缚去,第一次騎總是會(huì)摔倒膊畴,還需要實(shí)踐,閱讀大量的代碼病游,觀(guān)察并思考他人的失敗和成功唇跨,甚至是從自己的失敗中的出經(jīng)驗(yàn)稠通,當(dāng)你因?yàn)樽约夯靵y的代碼而付出代碼,一定會(huì)牢記在心买猖。二者缺一不可改橘。這里要描述的只是其中的一小部分。

寫(xiě)出好代碼玉控,關(guān)注軟件的內(nèi)部質(zhì)量是很重要的飞主,如果只關(guān)注外部質(zhì)量而不關(guān)注內(nèi)部質(zhì)量,隨著項(xiàng)目不斷的開(kāi)發(fā)高诺,需求日益復(fù)雜碌识,或因時(shí)間而遺忘細(xì)節(jié),會(huì)導(dǎo)致代碼越來(lái)越難以理解虱而,維護(hù)筏餐,修改,積攢的bug越來(lái)越多牡拇,逐漸變成人人避之不及的大坑魁瞪。

一開(kāi)始就關(guān)注代碼質(zhì)量是很有效的解決辦法,

Later equals never

這一點(diǎn)我們?cè)趯?shí)際開(kāi)發(fā)中應(yīng)該深有體會(huì)惠呼,所有想要留到以后解決的基本上都會(huì)隨著時(shí)間變多导俘,然后被遺忘。

這里引用一下代碼整潔之道里面的一段總結(jié)

代碼能工作還不夠剔蹋。能工作的代碼經(jīng)常會(huì)嚴(yán)重崩潰旅薄,滿(mǎn)足于僅僅讓代碼能工作的程序員不夠?qū)I(yè)。他們會(huì)害怕沒(méi)時(shí)間改進(jìn)代碼的結(jié)構(gòu)和設(shè)計(jì)泣崩。沒(méi)什么能比糟糕的代碼給開(kāi)發(fā)項(xiàng)目帶來(lái)更深遠(yuǎn)和長(zhǎng)期的損害了赋秀。進(jìn)度可以重訂,需求可以重新定義律想,團(tuán)隊(duì)動(dòng)態(tài)可以修正猎莲,但糟糕的代碼只是一直腐敗發(fā)酵,無(wú)情的拖著團(tuán)隊(duì)的后腿技即。

當(dāng)然著洼,糟糕的代碼可以清理液荸。不過(guò)成本高昂伤柄,隨著代碼腐敗下去适刀,模塊之間互相滲透常挚,出現(xiàn)大量隱藏糾結(jié)的依賴(lài)關(guān)系秧倾。找到和破處陳舊的依賴(lài)關(guān)系又費(fèi)時(shí)間又費(fèi)勁傀缩。另一方面料身,保持代碼整潔卻相對(duì)容易楞慈。早晨在模塊中制造出一堆混亂聚霜,下午就能輕易清理掉报嵌。更好的情況是血筑,5分鐘之前制造出混亂绘沉,馬上就能清理掉。

Overview

回到正題豺总,單元測(cè)試车伞。

說(shuō)起單元測(cè)試,可能很多人的第一印象是喻喳,會(huì)寫(xiě)單元測(cè)試的都是大神另玖,單元測(cè)試很有用,但是我不會(huì)寫(xiě)表伦,也不需要谦去,我的代碼能跑起來(lái),也沒(méi)什么bug蹦哼,為什么還要花時(shí)間寫(xiě)單元測(cè)試鳄哭。

說(shuō)了這么多,那么到底為什么要寫(xiě)單元測(cè)試纲熏?

Why

首先妆丘,單元測(cè)試可以給我們最直接的反饋,哪里出錯(cuò)了赤套,哪里寫(xiě)的不對(duì)飘痛,只要運(yùn)行一下,馬上就會(huì)知道容握,現(xiàn)代開(kāi)發(fā)工具都提供的對(duì)單元測(cè)試的集成宣脉,點(diǎn)一下就可以運(yùn)行并知道結(jié)果,這要比代碼在整個(gè)項(xiàng)目中實(shí)際運(yùn)行時(shí)要簡(jiǎn)單的多剔氏。

其次塑猖,單元測(cè)試可以對(duì)我們提供一層保護(hù)措施竹祷,讓你可以隨意的重構(gòu),修改功能羊苟,優(yōu)化代碼塑陵,非常清晰直觀(guān)的讓你知道到底有沒(méi)有問(wèn)題,你的改動(dòng)到底有沒(méi)有破壞原有的功能蜡励,是否產(chǎn)生產(chǎn)生了隱藏的問(wèn)題令花,一切盡在掌握之中。

最后凉倚,單元測(cè)試也是對(duì)了解業(yè)務(wù)提供了重要的幫助兼都。好的單元測(cè)試邏輯簡(jiǎn)單,通過(guò)單元測(cè)試了解代碼中實(shí)際實(shí)現(xiàn)的業(yè)務(wù)是非常容易的稽寒,尤其對(duì)很多沒(méi)有詳細(xì)需求文檔的項(xiàng)目扮碧。在新接手一個(gè)項(xiàng)目,或當(dāng)你忘記的以前寫(xiě)的內(nèi)容時(shí)杏糙,閱讀單元測(cè)試是快速了解業(yè)務(wù)的重要途徑慎王。

What

下面看什么是單元測(cè)試,先看一張圖宏侍。

pyramid.png

在軟件測(cè)試中有這樣一個(gè)金字塔概念赖淤,可以看到分了三層,每一層代碼一種不同類(lèi)型的測(cè)試负芋。

最底層的就是unit test漫蛔,是我們最經(jīng)常會(huì)大量寫(xiě)到的功能專(zhuān)一的測(cè)試嗜愈,直接運(yùn)行在本地環(huán)境旧蛾;integration test和UI test 則是需要運(yùn)行在真實(shí)的環(huán)境上,也就是手機(jī)或虛擬機(jī)上蠕嫁。對(duì)應(yīng)在Android項(xiàng)目中的是test和andoridTest兩種測(cè)試锨天。后兩種可以告訴你你的軟件是不是實(shí)際上功能正常,相對(duì)來(lái)說(shuō)速度要慢剃毒,因?yàn)樾枰虬蒩pk病袄,安裝到真實(shí)環(huán)境中,通過(guò)類(lèi)似于用戶(hù)交互的方式來(lái)測(cè)試赘阀。

單元測(cè)試益缠,顧名思義是在一個(gè)相對(duì)獨(dú)立的狀態(tài)下對(duì)單元(Unit)進(jìn)行測(cè)試。
編寫(xiě)單元測(cè)試有幾個(gè)特點(diǎn):

  1. 深入透徹基公,但是避免過(guò)度設(shè)計(jì)幅慌,缺乏設(shè)計(jì)且頻繁變更的功能很難編寫(xiě)詳盡的單元測(cè)試

  2. 隔離外部環(huán)境,可重復(fù)運(yùn)行轰豆,外部因素需要排除胰伍,如接口訪(fǎng)問(wèn)網(wǎng)絡(luò)和數(shù)據(jù)庫(kù)齿诞,在有無(wú)網(wǎng)絡(luò)時(shí)測(cè)試結(jié)果應(yīng)相同

  3. 功能單一,單元測(cè)試需要保持功能簡(jiǎn)潔單一骂租,一個(gè)測(cè)試只專(zhuān)注于單一功能

  4. 驗(yàn)證行為而非實(shí)現(xiàn)祷杈,避免在實(shí)現(xiàn)改動(dòng)后需要重新編寫(xiě)測(cè)試代碼。在Andorid中渗饮,因?yàn)榇蟛糠謫卧獪y(cè)試是沒(méi)有實(shí)際的ui的但汞,這一方法更是尤為重要,我們通常需要使用MVP或MVVM等類(lèi)似的架構(gòu)來(lái)實(shí)現(xiàn)互站。

    看下面的代碼特占,我們需要測(cè)試一個(gè)登錄后顯示提示的功能是否正常,這里使用MVP架構(gòu)云茸,通過(guò)mockito框架輕松mock一個(gè)View接口是目,在登錄方法被調(diào)用之后驗(yàn)證是否調(diào)用了一次showLoginHint方法。之后無(wú)論view的實(shí)現(xiàn)如何改變标捺,這段測(cè)試代碼都不需要被修改了懊纳。

    public class LoginPresenter {
    
        private View view;
    
        public LoginPresenter(View view) {
            this.view = view;
        }
    
        public void login() {
            this.view.showLoginHint();
        }
    
        interface View {
            void showLoginHint();
        }
    }
    
    public class LoginPresenterTest {
    
        public void testLogin() {
            LoginPresenter.View view = mock(LoginPresenter.View.class);
            new LoginPresenter(view).login();
            verify(view).showLoginHint();
        }
    }
    
  5. 快速,編寫(xiě)測(cè)試代碼需要頻繁的運(yùn)行亡容,所以速度很重要嗤疯,這也是為什么會(huì)使用robolectric來(lái)在本地JDK上模擬Android運(yùn)行環(huán)境的原因

  6. 簡(jiǎn)潔,避免復(fù)雜邏輯闺兢,測(cè)試代碼應(yīng)該一目了然茂缚,這也是很多測(cè)試庫(kù)所實(shí)現(xiàn)的目的,雖然編寫(xiě)單元測(cè)試可能會(huì)多寫(xiě)一些代碼屋谭,但大多數(shù)應(yīng)該只具備簡(jiǎn)單邏輯脚囊,即輸入-輸出-校驗(yàn)

提起單元測(cè)試,另一個(gè)經(jīng)常被放在一起的就是Test-driven development (TDD)

TDD簡(jiǎn)單的說(shuō)就是邊寫(xiě)測(cè)試邊寫(xiě)代碼桐磁,流程如下圖

testing-workflow.png
  1. 增加測(cè)試
  2. 運(yùn)行測(cè)試找出錯(cuò)誤
  3. 編寫(xiě)代碼使測(cè)試能通過(guò)
  4. 運(yùn)行測(cè)試
  5. 重構(gòu)代碼悔耘,提高代碼質(zhì)量
  6. 重復(fù)上述操作

通過(guò)快速迭代的開(kāi)發(fā)流程,來(lái)確保每一步的正確性我擂,其實(shí)和我們平時(shí)寫(xiě)幾行代碼就運(yùn)行一下程序試一試效果是差不多的衬以,把上面的編寫(xiě)測(cè)試和運(yùn)行測(cè)試的步驟,換成"在手機(jī)上運(yùn)行一下程序校摩,點(diǎn)一點(diǎn)新功能或看控制臺(tái)的日志看峻,確定功能是否正確"是不是很像平時(shí)的操作,當(dāng)然大部分人可能省略了第5條衙吩。還記得上面單元測(cè)試的幾個(gè)特點(diǎn)么互妓,點(diǎn)一點(diǎn)測(cè)試和看日志可不具備這么多功能。

科普時(shí)間

behavior-driven development (BDD)

行為驅(qū)動(dòng)開(kāi)發(fā),由TDD衍生而來(lái)车猬,鼓勵(lì)讓開(kāi)發(fā)人員和非技術(shù)人員協(xié)作霉猛,通過(guò)自然語(yǔ)言實(shí)現(xiàn)非技術(shù)人員也能看懂的測(cè)試流程。

BDD的測(cè)試用例就像講故事珠闰,開(kāi)發(fā)人員和測(cè)試惜浅,項(xiàng)目經(jīng)理等非技術(shù)人員經(jīng)過(guò)討論將一系列需求寫(xiě)成故事中的一個(gè)個(gè)場(chǎng)景。首先定義一套模板伏嗜,讓非技術(shù)人員通過(guò)模板寫(xiě)出要測(cè)試的內(nèi)容坛悉,然后有開(kāi)發(fā)人員完成實(shí)現(xiàn)的代碼。測(cè)試的流程大致是這樣:

Feature: Book Search
    Scenario: Search books by author
      Given there's a book called "Tips for job interviews" written by "John Smith"
        And there's a book called "Bananas and their many colors" written by "James Doe"
        And there's a book called "Mama look I'm a rock star" written by "John Smith"
      When an employee searches by author "John Smith"
      Then 2 books should be found
        And Book 1 has the title "Tips for job interviews"
        And Book 2 has the title "Mama look I'm a rock star"

How to write testable code part 1 相關(guān)知識(shí)補(bǔ)充

在開(kāi)始說(shuō)如何寫(xiě)出可測(cè)試的代碼之前承绸,先補(bǔ)充一些理論知識(shí)裸影。

S.O.L.I.D

軟件開(kāi)發(fā)中的5個(gè)原則,當(dāng)這些原則被一起應(yīng)用時(shí)军熏,它們使得一個(gè)程序員開(kāi)發(fā)一個(gè)容易進(jìn)行軟件維護(hù)和擴(kuò)展的系統(tǒng)變得更加可能耕腾。這一概念由 Robert C. Martin 提出依沮。

Robert_Cecil_Martin.png

Robert C. Martin 應(yīng)該是非常著名的專(zhuān)注于面向?qū)ο笫崖撸艚蓍_(kāi)發(fā)局装,提高代碼質(zhì)量的專(zhuān)家,還有一個(gè)名字是Uncle Bob摩幔,他寫(xiě)的書(shū)有很多經(jīng)典著作彤委,比如我最近在看的代碼整潔之道。

  1. 單一職責(zé)原則 Single responsibility principle

    單一職責(zé)模式告訴我們或衡,A class should have only one reason to change. 一個(gè)類(lèi)有且僅有一個(gè)原因使其被修改焦影。說(shuō)人話(huà)是我簡(jiǎn)單,我快樂(lè)封断。該原則說(shuō)明了兩點(diǎn)斯辰,一是一個(gè)類(lèi)只能有一個(gè)職責(zé),二是只能有一個(gè)修改的理由澄港。舉個(gè)例子椒涯,我們常用的日志模塊柄沮,如果一個(gè)類(lèi)既包含打印日志內(nèi)容的功能回梧,又包含日志格式的功能,那么在修改其中任何一個(gè)功能時(shí)祖搓,勢(shì)必會(huì)影響到另外一個(gè)功能狱意,在復(fù)雜的真實(shí)環(huán)境中,這種影響可能會(huì)造成很?chē)?yán)重的潛在問(wèn)題拯欧。

    單一職責(zé)原則是面向?qū)ο笾袠O為重要的一條详囤,非常容易理解和遵循,然而卻時(shí)常遭到破壞的原則。我們經(jīng)常忙于應(yīng)付多變的需求和即將臨近deadline藏姐,而忽略對(duì)代碼的組織結(jié)構(gòu)的保持隆箩。有的人會(huì)覺(jué)得過(guò)多的類(lèi)會(huì)導(dǎo)致系統(tǒng)過(guò)于復(fù)雜,想要找一個(gè)功能需要在好多類(lèi)之間跳來(lái)跳去羔杨。如果你的項(xiàng)目只有很少的代碼捌臊,的確放在一起要更容易找到自己的目標(biāo),然而大部分真實(shí)項(xiàng)目都具備相當(dāng)復(fù)雜的邏輯兜材,包含各種龐雜的功能理澎,把多個(gè)功能放在一起并不會(huì)簡(jiǎn)化你的代碼,問(wèn)題在于曙寡,在你有很多東西時(shí)糠爬,你想要用僅有的幾個(gè)大抽屜裝下所有的東西,在找的時(shí)候翻的亂七八糟還不一定能找到举庶,還是想要有多個(gè)只裝一類(lèi)東西并且具有良好的分類(lèi)信息的小抽屜呢执隧?大部分時(shí)間我們開(kāi)發(fā)或者維護(hù)某個(gè)功能時(shí),僅僅只需要關(guān)心相關(guān)的一些邏輯而已户侥,并不需要知道其余任何無(wú)關(guān)功能殴玛。每個(gè)達(dá)到一定規(guī)模的系統(tǒng)都具有相當(dāng)?shù)膹?fù)雜性,對(duì)這種復(fù)雜性的良好管理使我們能快速準(zhǔn)確的找到需要的類(lèi)添祸,而不是在一大堆無(wú)關(guān)的功能中反復(fù)尋找自己需要的代碼滚粟。

    遵循單一職責(zé)原則要求我們?cè)陂_(kāi)發(fā)前就要對(duì)職責(zé)進(jìn)行劃分,分而治之刃泌,無(wú)論是提前規(guī)劃好凡壤,還是當(dāng)發(fā)現(xiàn)一個(gè)類(lèi)有多個(gè)職責(zé)時(shí)進(jìn)行修改,都會(huì)幫助我們更好的理解和創(chuàng)建抽象耙替,我們常說(shuō)高內(nèi)聚低耦合亚侠,單一職責(zé)模式就是對(duì)高內(nèi)聚很好的詮釋。

  2. 開(kāi)閉原則 Open/closed principle

    開(kāi)閉原則規(guī)定“軟件中的對(duì)象(類(lèi)俗扇,模塊硝烂,函數(shù)等等)應(yīng)該對(duì)于擴(kuò)展是開(kāi)放的,但是對(duì)于修改是封閉的"

    這個(gè)原則個(gè)人感覺(jué)是最不好實(shí)現(xiàn)的铜幽,也是最難理解的滞谢,對(duì)修改封閉不代表完全沒(méi)有修改,那么什么情況應(yīng)該允許修改除抛,什么情況需要去擴(kuò)展狮杨,首先對(duì)于設(shè)計(jì)是一個(gè)巨大的考驗(yàn),前期設(shè)計(jì)的失敗可能導(dǎo)致后期擴(kuò)展時(shí)花費(fèi)大量時(shí)間修改到忽,當(dāng)然沒(méi)有一蹴而就的代碼橄教,開(kāi)發(fā)過(guò)程中需要權(quán)衡利弊,想要達(dá)成最優(yōu)效果,需要具備良好的抽象能力护蝶,掌握大量的設(shè)計(jì)模式华烟,其他幾條原則也同樣可以幫助我們更好的實(shí)現(xiàn)開(kāi)閉原則。

    其實(shí)開(kāi)閉原則的關(guān)鍵持灰,是用你對(duì)各種設(shè)計(jì)模式的理解程度和抽象能力垦江,符合其他原則及各種設(shè)計(jì)模式,自然也就符合開(kāi)閉原則搅方,其他的原則及設(shè)計(jì)模式恰恰是告訴你如何實(shí)現(xiàn)開(kāi)閉模式的細(xì)節(jié)比吭。

  3. 里氏替換原則 Liskov substitution principle

    子類(lèi)對(duì)象使用的地方都可以替換為父類(lèi)對(duì)象,且能保持邏輯不變姨涡。

    作為5個(gè)原則里唯一以人名命名的衩藤,是由兩個(gè)非常厲害的人于1993年提出的。

    Barbara_Liskov.jpg

    Barbara Liskov, 美國(guó)計(jì)算機(jī)科學(xué)家涛漂,2008年圖靈獎(jiǎng)得主赏表,2004年約翰·馮諾依曼獎(jiǎng)得主。現(xiàn)任麻省理工學(xué)院電子電氣與計(jì)算機(jī)科學(xué)系教授匈仗。

    Jeannette_Wing.jpg

    周以真瓢剿,微軟全球資深副總裁,美國(guó)計(jì)算機(jī)科學(xué)家悠轩〖淇瘢卡內(nèi)基梅隆大學(xué)教授。美國(guó)國(guó)家自然基金會(huì)計(jì)算與信息科學(xué)工程部助理部長(zhǎng)火架。ACM和IEEE會(huì)士鉴象。

    繼承,多態(tài)等是面向?qū)ο笳Z(yǔ)言的特性何鸡,都是經(jīng)常使用的纺弊。符合里氏替換原則的實(shí)現(xiàn)包含這樣一層含義,父類(lèi)中實(shí)現(xiàn)好的方法骡男,都是符合一定的契約和規(guī)范的淆游,子類(lèi)實(shí)現(xiàn)的時(shí)候不能違反,否則會(huì)對(duì)整個(gè)繼承體系造成破壞隔盛。難點(diǎn)在于既要提前規(guī)劃好后續(xù)可能發(fā)生的情況犹菱,對(duì)類(lèi)的功能進(jìn)行抽象,又要防止過(guò)度設(shè)計(jì)骚亿,避免設(shè)計(jì)的過(guò)于復(fù)雜影響后續(xù)開(kāi)發(fā)維護(hù)已亥。比較穩(wěn)妥的方式通常是只實(shí)現(xiàn)父類(lèi)的抽象方法,這要求我們?cè)诔跗谠O(shè)計(jì)時(shí)就要考慮到這些来屠,或者干脆不使用繼承,而是組合的方式來(lái)實(shí)現(xiàn)某些功能來(lái)避免耦合。

    使用繼承的優(yōu)點(diǎn)在于減少重復(fù)代碼俱笛,直接給子類(lèi)提供了父類(lèi)的功能捆姜,正所謂龍生龍,鳳生鳳迎膜,老鼠的兒子會(huì)打洞泥技,然而有點(diǎn)也即是缺點(diǎn),子類(lèi)同樣耦合了父類(lèi)的功能磕仅,降低了靈活性珊豹,子類(lèi)要考慮是否違反了父類(lèi)的規(guī)范,父類(lèi)修改時(shí)要考慮是否對(duì)子類(lèi)造成影響榕订,稍有不慎店茶,可能就會(huì)搞出龍生耗子這樣的問(wèn)題,屆時(shí)就需要對(duì)原有代碼大量的重構(gòu)劫恒,尤其在缺乏規(guī)范的情況下更是如此贩幻。

    一個(gè)經(jīng)典例子是長(zhǎng)方形和正方形,看下面你的代碼两嘴,正方形繼承了長(zhǎng)方形丛楚,為了保持正方形的寬高一致,我們重寫(xiě)了父類(lèi)的方法憔辫,看起來(lái)是沒(méi)什么問(wèn)題趣些。但是如果我們基于"長(zhǎng)方形的面積=長(zhǎng)*寬"這一規(guī)則來(lái)測(cè)試正方形,在設(shè)置了長(zhǎng)或?qū)捴蠓∧琣rea方法就不符合預(yù)期結(jié)果了喧务。

    public class Rectangle {
        private int width;
        private int height;
    
        public void setWidth(int width) {
            this.width = width;
        }
    
        public void setHeight(int height) {
            this.height = height;
        }
    
        public int area() {
            return this.height * this.width;
        }
    }
    
    public class Square extends Rectangle {
        @Override
        public void setWidth(int width) {
            super.setWidth(width);
            super.setHeight(width);
        }
    
        @Override
        public void setHeight(int height) {
            super.setWidth(height);
            super.setHeight(height);
        }
    }
    

    下面我們來(lái)進(jìn)行修改,一個(gè)可行的辦法是像下面這樣修改枉圃,將width和height的set方法去掉功茴,由constructo直接傳入,這樣就符合了父類(lèi)的契約孽亲。

      public class Rectangle {
        private int width;
        private int height;
    
        public Rectangle(int width, int height) {
            this.width = width;
            this.height = height;
        }
    
        public int getHeight() {
            return height;
        }
    
        public int getWidth() {
            return width;
        }
    
        public int area() {
            return this.height * this.width;
        }
    }
    
    public class Square extends Rectangle {
        public Square(int width) {
            super(width, width);
        }
    }
    

    Java中實(shí)現(xiàn)的非常經(jīng)典的例子:

    Collection

    如使用List的坎穿,不管實(shí)現(xiàn)類(lèi)是ArrayList,還是LinkedList返劲,變量類(lèi)型全用List就可以玲昧。

    Stream

    很多方法參數(shù)或返回值給一個(gè)OutpuStream或InputStream,什么實(shí)現(xiàn)類(lèi)并不影響代碼邏輯篮绿,你甚至都不知道實(shí)現(xiàn)類(lèi)是什么

  4. 接口隔離原則 Interface segregation principle

    接口隔離原則規(guī)定孵延,使用者不應(yīng)該被強(qiáng)制依賴(lài)不需要的方法。大而全的接口應(yīng)當(dāng)被拆分長(zhǎng)更細(xì)粒度的接口亲配,這樣使用者可以?xún)H僅依賴(lài)需要的部分尘应,減少耦合惶凝,使其更易于重構(gòu)和修改。當(dāng)然使用的時(shí)候同樣需要權(quán)衡犬钢,拆分接口并不意味著越細(xì)粒度越好苍鲜,應(yīng)該根據(jù)需要,選擇合適的方法進(jìn)行組合玷犹。

    從來(lái)自wiki的說(shuō)明我們得知混滔,

    這條原則最初的構(gòu)想來(lái)自Robert C. Martin在施樂(lè)公司期間的一個(gè)打印機(jī)程序,在他們發(fā)現(xiàn)這個(gè)程序幾乎無(wú)法被維護(hù)時(shí)歹颓,試圖找出原因坯屿。有一個(gè)Job類(lèi)貫穿了整個(gè)系統(tǒng),打印機(jī)需要執(zhí)行的每個(gè)任務(wù)巍扛,都要調(diào)用Job類(lèi)领跛,這導(dǎo)致一個(gè)fat class 包含了大量的各種功能不同功能的方法。由于這個(gè)設(shè)計(jì)电湘,任何一個(gè)任務(wù)都要知道全部的方法隔节,即使根本不需要。解決辦法是按照依賴(lài)倒置原則寂呛,增加了一層接口層怎诫,把巨大的Job類(lèi)替換為一個(gè)個(gè)接口,對(duì)應(yīng)每個(gè)任務(wù)贷痪,任務(wù)去調(diào)用對(duì)應(yīng)的接口來(lái)實(shí)現(xiàn)功能幻妓,當(dāng)然這些接口的實(shí)現(xiàn)都在Job類(lèi)中。

    我根據(jù)這個(gè)例子畫(huà)了個(gè)圖:

    改動(dòng)前:

    printer1.png

    改動(dòng)后:

    printer2.png

    可以看出后面的改動(dòng)很好的通過(guò)接口將巨大的job類(lèi)分割成了一個(gè)一個(gè)小功能劫拢,屏蔽掉了調(diào)用者不需要知道的功能肉津。

  5. 依賴(lài)倒置原則 Dependency inversion principle

    依賴(lài)抽象而非實(shí)現(xiàn),這個(gè)在依賴(lài)注入里面說(shuō)過(guò)了舱沧,這里就不再重復(fù)了妹沙。

迪米特法則 / 最少知識(shí)原則 Law of Demeter (LoD) or principle of least knowledge

得墨忒耳定律(Law of Demeter,縮寫(xiě)LoD)亦稱(chēng)為“最少知識(shí)原則(Principle of Least Knowledge)”熟吏。
在198x年距糖,有一群程序員開(kāi)發(fā)了一個(gè)系統(tǒng)來(lái)研究如何使面向?qū)ο笳Z(yǔ)言開(kāi)發(fā)的軟件更易于維護(hù)和修改,名字是The Demeter Project牵寺,一個(gè)關(guān)于Aspect-Oriented Software Development (AOSD)的項(xiàng)目悍引,在此期間發(fā)現(xiàn)了得墨忒爾定律。Law of Demeter 的名字由此而來(lái)帽氓。Demeter是希臘神話(huà)中的農(nóng)業(yè)女神趣斤,得墨忒耳。

遵循得墨忒耳定律可以使你的代碼松耦合黎休,提高代碼的復(fù)用程度浓领,更易于維護(hù)玉凯,更易于測(cè)試。關(guān)鍵點(diǎn)是镊逝,只和自己直接的朋友交流壮啊。

簡(jiǎn)單的來(lái)說(shuō)就是:

  • 你的方法只能調(diào)用自己相同類(lèi)中的方法(Java: this)
  • 你的方法只能調(diào)用自己所屬對(duì)象中的屬性的方法(Java: fields)
  • 如果你的方法有參數(shù)傳遞進(jìn)來(lái)嫉鲸,可以調(diào)用參數(shù)的方法
  • 如果你的方法創(chuàng)建了一個(gè)本地對(duì)象撑蒜,可以調(diào)用這個(gè)對(duì)象的方法
  • 不應(yīng)該調(diào)用全局對(duì)象,但是可以作為參數(shù)傳遞進(jìn)來(lái)
  • 不應(yīng)該有鏈?zhǔn)秸{(diào)用玄渗,a.getB().getC().doSomething()應(yīng)該放在a對(duì)象里

注意最后一條座菠,如果把a(bǔ).getB().getC().doSomething()改成a.doSomething(),仍然違反了得墨忒耳定律藤树,因?yàn)閍里面會(huì)有b.getC().doSomething()浴滴,所以應(yīng)該有一個(gè)doSomething方法在b類(lèi)中,a.doSomething()調(diào)用b.doSomethine()岁钓。

按照上述方式升略,一個(gè)對(duì)象的內(nèi)部結(jié)構(gòu),運(yùn)作方式只有它自己才知道屡限,因此也稱(chēng)最少知識(shí)原則品嚣。通過(guò)封裝的方式顯著降低了代碼的耦合程度,類(lèi)與類(lèi)之間的影響降到了最低钧大,降低了修改的風(fēng)險(xiǎn)翰撑。

在分層架構(gòu)中,遵循得墨忒耳定律意味著每一層的代碼僅能調(diào)用自己本層或下一層中的方法啊央。越俎代庖這個(gè)詞用來(lái)形容違反了LoD原則最合適不過(guò)了眶诈。

然而,遵循得墨忒耳定律可能會(huì)產(chǎn)生大量的wrapper類(lèi)和方法用來(lái)傳遞跨越多個(gè)多個(gè)對(duì)象間方法的調(diào)用(propagation patterns)瓜饥,增加開(kāi)發(fā)維護(hù)的成本逝撬。一個(gè)相關(guān)的技術(shù)是AOP,這里簡(jiǎn)單的說(shuō)一下aspect-oriented programming (AOP)乓土,AOP框架可以在你的代碼中插入一些代碼片段宪潮,而這個(gè)過(guò)程都是自動(dòng)生成的,避免了手工書(shū)寫(xiě)代碼的繁瑣帐我。因此坎炼,得墨忒爾定律配合AOP食用更佳。

另一個(gè)和得墨忒爾定律相關(guān)的是訪(fǎng)問(wèn)者模式(VisitorPattern)拦键,當(dāng)你需要深入一個(gè)對(duì)象的內(nèi)部結(jié)構(gòu)去調(diào)用一系列方法時(shí)谣光,用訪(fǎng)問(wèn)者模式對(duì)其進(jìn)行封裝是一個(gè)很好的方式。

如果看了上面的不是很理解芬为,某Android群大神給我說(shuō)了一個(gè)更通俗的例子:

你和你未來(lái)的老婆不認(rèn)識(shí)萄金,但是你認(rèn)識(shí)她的閨蜜蟀悦,你和你未來(lái)的老婆不能直接交流,因?yàn)槟悴徽J(rèn)識(shí)她氧敢。有一天你突然想和你未來(lái)的老婆認(rèn)識(shí)認(rèn)識(shí)日戈,那么你首先要通過(guò)她的閨蜜把你介紹給她,然后有兩個(gè)方案孙乖,一個(gè)是始終通過(guò)閨蜜給她傳話(huà)浙炼,或者把她變成你的老婆,這樣她就是和你具有直接關(guān)系的對(duì)象了唯袄,也就可以直接和她交流了弯屈。

Summary

上面的6個(gè)規(guī)則只是程序設(shè)計(jì)中的一部分,想要寫(xiě)出高質(zhì)量的代碼恋拷,除了學(xué)習(xí)設(shè)計(jì)模式之外资厉,還需要大量的實(shí)踐,理論畢竟只是理論蔬顾,在實(shí)際應(yīng)用中達(dá)成的效果可能千差萬(wàn)別宴偿,任何模式的應(yīng)用,都需要仔細(xì)權(quán)衡利弊诀豁,避免過(guò)度設(shè)計(jì)窄刘,也要防止設(shè)計(jì)過(guò)于簡(jiǎn)陋或干脆不進(jìn)行任何設(shè)計(jì),導(dǎo)致代碼難以維護(hù)和擴(kuò)展且叁。

其實(shí)我們平時(shí)所做的程序設(shè)計(jì)都哭,大都離不開(kāi)三步,抽象逞带,分解欺矫,組合,也既是計(jì)算思維展氓。上述6個(gè)原則及各種設(shè)計(jì)模式穆趴,只不過(guò)是過(guò)去無(wú)數(shù)人對(duì)以往經(jīng)驗(yàn)的總結(jié)提煉而出的幾種最優(yōu)解,讓你站在巨人的肩膀上遇汞,更好的實(shí)現(xiàn)這三步而已未妹。

How to write testable code part 2 Best practice

很多時(shí)候我們看著代碼卻不知道怎么寫(xiě)單元測(cè)試,想要測(cè)試一個(gè)單一的方法空入,卻有無(wú)數(shù)的耦合對(duì)象影響络它,下面列舉一些讓我們更難進(jìn)行單元測(cè)試的關(guān)鍵點(diǎn)。

  1. 區(qū)分創(chuàng)建對(duì)象代碼和邏輯代碼
    編寫(xiě)測(cè)試代碼通常是歪赢,初始化應(yīng)用的一部分功能化戳,然后執(zhí)行某個(gè)操作,判斷這個(gè)功能的現(xiàn)象是否符合預(yù)期埋凯。對(duì)單元測(cè)試而言点楼,簡(jiǎn)單的說(shuō)就是創(chuàng)建一個(gè)對(duì)象扫尖,調(diào)用一個(gè)方法,根據(jù)返回結(jié)果或某些現(xiàn)象進(jìn)行斷言掠廓,以確定功能是否符合預(yù)期結(jié)果换怖。前面我們說(shuō)過(guò),單元測(cè)試的是在隔離的環(huán)境中對(duì)類(lèi)(Unit)進(jìn)行測(cè)試蟀瞧,換言之就是不能讓其他對(duì)象對(duì)我們的運(yùn)行結(jié)果產(chǎn)生隨機(jī)影響沉颂,這就需要我們要測(cè)試的對(duì)象沒(méi)有在內(nèi)部自行創(chuàng)建(new)其他對(duì)象,否則就可能會(huì)對(duì)結(jié)果造成未知的影響黄橘。

    還記得上面的單一職責(zé)原則么兆览,我們可以將代碼按內(nèi)容分為兩種職責(zé)屈溉,一種是創(chuàng)建對(duì)象(因?yàn)榇蟛糠智闆r我們不會(huì)創(chuàng)建一個(gè)完全獨(dú)立的對(duì)象塞关,大多數(shù)對(duì)象都存在一些依賴(lài)關(guān)系,因此稱(chēng)之為 object graph)的代碼子巾,一種是功能邏輯的代碼帆赢。將這兩種代碼分別封裝在兩種類(lèi)中,負(fù)責(zé)創(chuàng)建對(duì)象的類(lèi)线梗,根據(jù)應(yīng)用的設(shè)計(jì)模式不同椰于,可能為factory/provider等,和負(fù)責(zé)掌管功能邏輯類(lèi)仪搔。當(dāng)然也可以使用其他手段瘾婿,如依賴(lài)注入框架來(lái)輔助我們實(shí)現(xiàn)這樣的功能。經(jīng)過(guò)這樣修改之后烤咧,我們就可以自由的創(chuàng)建需要測(cè)試的對(duì)象偏陪,把其他對(duì)象可以輕易的被替換為"假"的對(duì)象,使其的行為符合我們預(yù)設(shè)的邏輯煮嫌。使用mock框架會(huì)使這一步驟更為簡(jiǎn)單笛谦。

  2. 遵循Law of Demeter

    如果你按照上面第一條的進(jìn)行改造,避免自己new新對(duì)象昌阿,就會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題饥脑,我不在類(lèi)里面自己new對(duì)象了,那我要用到其他的類(lèi)怎么辦呢懦冰?當(dāng)然是伸手要(作為構(gòu)造參數(shù)傳進(jìn)來(lái))灶轰。如果你使用過(guò)依賴(lài)注入,這個(gè)思路就很熟悉了:我們不創(chuàng)建對(duì)象刷钢,而是像別人要對(duì)象笋颤。

    注意別忘了德墨忒爾定律,

    違反得墨忒爾定律就像在干草堆里尋找一根針闯捎。

    只能使用自己有直接接觸的對(duì)象椰弊,不要傳遞一個(gè)萬(wàn)能的對(duì)象進(jìn)來(lái)(kitchen sink / fat class / god class)许溅,否則你可能需要?jiǎng)?chuàng)造大量的"假"對(duì)象來(lái)實(shí)現(xiàn)單元測(cè)試。還記得單元測(cè)試的特點(diǎn)么秉版,簡(jiǎn)潔贤重,簡(jiǎn)潔,簡(jiǎn)潔清焕,重要的事情說(shuō)三遍并蝗,復(fù)雜的邏輯和冗余的代碼會(huì)極大破壞單元測(cè)試的效果。違反得墨忒爾定律可能會(huì)使你的創(chuàng)造"假"對(duì)象的代碼量翻上好幾倍秸妥。

    雖然mockito之類(lèi)的mock框架提供了非常簡(jiǎn)單的方法來(lái)mock對(duì)象一個(gè)context之類(lèi)的god class滚停,然而

    1. 即便使用框架,每個(gè)mock對(duì)象仍然要寫(xiě)幾行代碼粥惧,越深入object graph键畴,mock對(duì)象代碼就要翻倍。

    2. 由于這些對(duì)象的耦合關(guān)聯(lián)很強(qiáng)突雪,導(dǎo)致測(cè)試代碼非常不健壯起惕,一旦對(duì)這些對(duì)象之間的內(nèi)部聯(lián)系做了某些修改,很可能就要重寫(xiě)單元測(cè)試

    3. 即時(shí)只需要context中的某一兩個(gè)類(lèi)咏删,仍然需要mock整個(gè)類(lèi)惹想,會(huì)存在大量冗余代碼,且遍布各個(gè)測(cè)試中督函,嚴(yán)重降低單元測(cè)試的可讀性嘀粱。

    學(xué)一個(gè)單詞kitchen sink,直譯時(shí)廚房水槽辰狡,在這里表示任何東西锋叨,可能的出處是第二次世界大戰(zhàn)期間軍隊(duì)中使用的俚語(yǔ),原文是'Out for blood, our Navy throws everything but the kitchen sink at Jap vessels, warships and transports alike.'搓译,表示除了廚房水槽之外的其他東西悲柱。

    上述內(nèi)容都是為了構(gòu)建一個(gè)隔離的環(huán)境供我們實(shí)現(xiàn)單元測(cè)試,我們來(lái)看下面的例子些己,創(chuàng)建一個(gè)House并進(jìn)行測(cè)試是非常簡(jiǎn)單的豌鸡,只需要new一個(gè)對(duì)象出來(lái),然后調(diào)用方法段标,對(duì)結(jié)果進(jìn)行斷言涯冠,就可以了。

    class House {
        private boolean isLocked;
        private boolean isLocked() {
            return isLocked;
        }
        private void lock() {
            isLocked = true;
        }
    }
    

    測(cè)試House類(lèi)如此簡(jiǎn)單是因?yàn)檫@是一個(gè)葉子類(lèi)逼庞,葉子類(lèi)的意思就是這個(gè)類(lèi)處于依賴(lài)樹(shù)的終點(diǎn)蛇更,它不依賴(lài)任何類(lèi)。所有的葉子類(lèi)都可以非常方便的進(jìn)行單元測(cè)試,但是其他類(lèi)就不這么輕松了派任,尤其是在內(nèi)部自行new對(duì)象的類(lèi)砸逊,因?yàn)樗箚卧獪y(cè)試不再處于一個(gè)隔離的環(huán)境中。

    這樣修改還有另一個(gè)好處掌逛,可以使你構(gòu)建object graph的過(guò)程更加清晰直觀(guān)师逸。

  3. 不要在Constructor中做太多事

    經(jīng)過(guò)上面兩條的修改,我們已經(jīng)去除了可能存在于Constructor中的new對(duì)象的邏輯豆混,但是還不止于此篓像。

    一個(gè)類(lèi)的單元測(cè)試可能多達(dá)幾十個(gè),測(cè)試中我們做的事情就是皿伺,每次創(chuàng)建一個(gè)對(duì)象不太一樣的對(duì)象员辩,執(zhí)行一個(gè)操作,斷言結(jié)果鸵鸥〉旎可以看到,最常做的的是創(chuàng)建對(duì)象脂男,為了使測(cè)試更快养叛,應(yīng)該避免在Constructor中做除了賦值之外的任何事,有的時(shí)候可能無(wú)所謂宰翅,但如果在其中做了過(guò)于復(fù)雜的事情,如從硬盤(pán)或者網(wǎng)絡(luò)讀取一些初始化設(shè)置爽室,一方面我們無(wú)法排除這些外部因素造成的不穩(wěn)定影響汁讼,另一方也會(huì)花費(fèi)大量的時(shí)間。

    如果我們的類(lèi)像這樣阔墩,就無(wú)法排除Door對(duì)測(cè)試帶來(lái)的影響嘿架。

    class House {
        private Door door;
    
        public House() {
            this.door = new Door();
        }
        // ...
    }
    

    可行的改進(jìn)辦法是:

    class House {
        private Door door;
    
        public House(Door door) {
            this.door = door;
        }
        // ...
    }
    

    這種方式我們就可以在測(cè)試中mock一個(gè)假door,并通過(guò)其行為來(lái)進(jìn)行判斷啸箫。

    同樣一些其他類(lèi)似的操作也應(yīng)當(dāng)杜絕耸彪,如initialization block,static block忘苛。如果真的需要一個(gè)復(fù)雜的初始化流程蝉娜,可以增加一個(gè)方法來(lái)手動(dòng)調(diào)用。不過(guò)這里偶爾會(huì)有一些極為個(gè)別的特例扎唾,通常是對(duì)一些read-only或write-only的對(duì)象或功能做簡(jiǎn)單的初始化召川,例如調(diào)用System.loadLibrary來(lái)加載so庫(kù),或者日志類(lèi)的初始化胸遇,需要注意的是荧呐,這種場(chǎng)景極為罕見(jiàn),大部分情況當(dāng)你試圖寫(xiě)在這里的代碼,都不是如此

    還需要注意的是倍阐,避免在初始化時(shí)訪(fǎng)問(wèn)全局狀態(tài)概疆,具體的原因可以看下面一條。

  4. 全局狀態(tài)

    全局狀態(tài)的作用域是整個(gè)應(yīng)用峰搪,我們上面講了這么多的模式届案,其目的最終都是為了達(dá)到高內(nèi)聚低耦合的效果,這就要求我們限制變量的作用域減少對(duì)全局狀態(tài)的使用罢艾。此外楣颠,全局狀態(tài)經(jīng)常是難以維護(hù)的,可能會(huì)增加閱讀理解代碼的難度咐蚯。在單元測(cè)試是童漩,多個(gè)測(cè)試如果訪(fǎng)問(wèn)同一個(gè)全局狀態(tài),可能會(huì)導(dǎo)致非預(yù)期效果春锋,產(chǎn)生偶然變化矫膨。雖然我們也可以將相關(guān)的測(cè)試按順序一個(gè)一個(gè)執(zhí)行,并且在每個(gè)測(cè)試開(kāi)始前重置全局狀態(tài)期奔,但是這無(wú)疑極大降低了測(cè)試的速度侧馅,增加了無(wú)關(guān)代碼和復(fù)雜程度,而且也不能保證測(cè)試的準(zhǔn)確性呐萌,例如某個(gè)功能依賴(lài)于全局狀態(tài)變化后的值馁痴,但是測(cè)試開(kāi)始前全局狀態(tài)被重置了。

    常量作為例外肺孤,是可以被允許存在的罗晕,因?yàn)槌A坑肋h(yuǎn)不會(huì)被修改。

  5. 單例

    其實(shí)單例也屬于全局狀態(tài)之一赠堵,屬于披著羊皮的狼小渊。既然我們不提倡使用全局狀態(tài),如果你讀懂了上面一條茫叭,自然也就明白使用單例的問(wèn)題所在了酬屉。可能有些人覺(jué)得單例和全局變量不一樣揍愁,那么換個(gè)思路呐萨,關(guān)鍵點(diǎn)是單例中保存這可修改的變量,這實(shí)際上等同于單個(gè)全局變量的聚合體吗垮!

    同樣有例外垛吗,不可變對(duì)象(immutable object)是被允許的,因?yàn)榫秃统A恳粯铀傅牵际遣粫?huì)被改變的怯屉。還有一種情況read-only/write-only的對(duì)象蔚舀,注意上面的說(shuō)的情況,全局狀態(tài)可能在任何位置被改變也可以在任何位置被讀取锨络,這意味這讀取和修改是不被控制的赌躺,而此種對(duì)象的單例只存在讀或?qū)懸环N操作,并不存在這種現(xiàn)象羡儿,一個(gè)常見(jiàn)的例子是日志類(lèi)礼患,日志類(lèi)只輸出日志,我們?cè)诔绦蛑胁魂P(guān)心輸出的內(nèi)容掠归,除非你要根據(jù)輸出的日志內(nèi)容做某些其他的操作缅叠,否則無(wú)論輸出是什么,都不影響代碼的邏輯功能虏冻。

  6. static method

    可以發(fā)現(xiàn)肤粱,單元測(cè)試大多是針對(duì)一個(gè)類(lèi)的方法進(jìn)行測(cè)試,多個(gè)類(lèi)通過(guò)依賴(lài)關(guān)系互相協(xié)作使我們可以從中截取一部分邏輯進(jìn)行單元測(cè)試厨相。靜態(tài)方法則破壞了這種方式领曼。我們無(wú)法分離某一部分的邏輯進(jìn)行測(cè)試÷回想一下面向?qū)ο笳Z(yǔ)言和傳統(tǒng)過(guò)程式語(yǔ)言的優(yōu)劣庶骄,不難理解為什么不提倡使用靜態(tài)方法。如果你的代碼有一大堆的復(fù)雜邏輯完全使用靜態(tài)方法實(shí)現(xiàn)践磅,使用傳統(tǒng)方式幾乎無(wú)法進(jìn)行單元測(cè)試单刁。mockito,easymock等框架均不支持mock靜態(tài)方法音诈,雖然powermock幻碱、dexmaker等框架的出現(xiàn),通過(guò)修改編譯后生成的字節(jié)碼或android的dex文件使mock靜態(tài)方法成為可能细溅,但考慮到面向?qū)ο笳Z(yǔ)言提供的種種優(yōu)勢(shì),將靜態(tài)方法中的邏輯封裝到一個(gè)或幾個(gè)對(duì)象中的做法無(wú)疑具有更好的擴(kuò)展性儡嘶,同時(shí)減少了可能存在的對(duì)全局狀態(tài)的訪(fǎng)問(wèn)喇聊,降低了維護(hù)的成本。

    對(duì)于葉子類(lèi)蹦狂,靜態(tài)方法是不存在任何問(wèn)題的誓篱,因?yàn)槿~子類(lèi)不依賴(lài)任何對(duì)象,可以輕易編寫(xiě)單元測(cè)試凯楔。

  7. 使用組合而非繼承

    組合提供了比繼承更靈活的方式來(lái)進(jìn)行擴(kuò)展窜骄,參考里氏替換原則,實(shí)現(xiàn)繼承無(wú)疑更具有挑戰(zhàn)性摆屯,缺乏足夠清晰的契約約束邻遏,或者存在錯(cuò)誤的設(shè)計(jì),繼承可能會(huì)導(dǎo)致更混亂的代碼。對(duì)單元測(cè)試而言准验,子類(lèi)耦合了父類(lèi)的功能赎线,錯(cuò)誤的繼承可能將不同的功能混在一起,導(dǎo)致我們無(wú)法單獨(dú)對(duì)某一個(gè)功能進(jìn)行單元測(cè)試糊饱。

  8. 使用多態(tài)而非條件語(yǔ)句

    有時(shí)我們會(huì)在一個(gè)方法中大量使用條件語(yǔ)句垂寥,if/else/switch,如果一個(gè)類(lèi)中有很多類(lèi)似的功能另锋,應(yīng)該考慮使用多態(tài)滞项,將一個(gè)類(lèi)將其分成幾個(gè)小一些的具有類(lèi)似行為的類(lèi),這樣我們可以針對(duì)每種狀態(tài)編寫(xiě)更簡(jiǎn)單更詳細(xì)的單元測(cè)試夭坪。

  9. 混合Service-objects和Value-objects類(lèi)

    通常代碼中會(huì)存在兩種類(lèi)型的對(duì)象文判,一種存儲(chǔ)數(shù)據(jù)的類(lèi),如bean類(lèi)台舱,也叫Value-objects律杠,pojo,dto等竞惋,或者是Map柜去,List等,通常不需要實(shí)現(xiàn)接口拆宛,很容易被創(chuàng)建嗓奢,也不需要被mock。另一種是封裝業(yè)務(wù)邏輯的類(lèi)浑厚,我們稱(chēng)之為Service-Objects股耽,這種類(lèi)在單元測(cè)試經(jīng)常需要被mock,經(jīng)常需要實(shí)現(xiàn)接口钳幅,使用各種復(fù)雜的設(shè)計(jì)模式來(lái)設(shè)計(jì)結(jié)構(gòu)物蝙。

    Value-objects也即是上面提到的葉子類(lèi),這些類(lèi)在測(cè)試時(shí)不需要被mock敢艰,直接new一個(gè)就可以诬乞,因此它們不能在內(nèi)部使用Service-objects。Service-objects通常更難以創(chuàng)建钠导,因?yàn)樗鼈冇懈鼜?fù)雜的邏輯關(guān)系震嫉,像上面第一條所描述的,這種類(lèi)不應(yīng)該在其內(nèi)部被自行創(chuàng)建新對(duì)象牡属,應(yīng)當(dāng)通過(guò)構(gòu)造函數(shù)傳遞進(jìn)來(lái),可以使用factory或者依賴(lài)注入框架實(shí)現(xiàn)逮栅。從測(cè)試的角度來(lái)看窗宇,我們更喜歡Value-objects担映,因?yàn)榭梢宰杂蓜?chuàng)建且易于測(cè)試矗蕊,Service-objects則由于復(fù)雜的依賴(lài)關(guān)系而變得難以測(cè)試短蜕,使得我們必須使用mock框架來(lái)模擬所有的依賴(lài)朋魔。將這兩種類(lèi)的功能混合在一起對(duì)兩者都沒(méi)有任何的好處。

  10. 小細(xì)節(jié)

    大部分時(shí)候如果我們的代碼遵循前面的6個(gè)原則卿操,編寫(xiě)單元測(cè)試都會(huì)相對(duì)更容易警检。如果你的類(lèi)或方法包含過(guò)多功能(違反單一職責(zé)原則),假如你的方法名字叫 xxxAndxxx害淤,那就要注意了扇雕,既無(wú)法讓他人快速的讀懂,也會(huì)增加測(cè)試的難度窥摄。同樣镶奉,簡(jiǎn)潔的代碼也更有利于單元測(cè)試,還要再說(shuō)一遍崭放,單元測(cè)試哨苛,顧名思義,是要對(duì) 單元 進(jìn)行的獨(dú)立的币砂,無(wú)干擾的測(cè)試建峭,我們所有的目的,最終都是為了構(gòu)筑功能單一的單元决摧,和排除其他不確定因素的干擾迹缀。

按照上面的小技巧來(lái)修改代碼,可以讓代碼變的更容易進(jìn)行單元測(cè)試蜜徽,更多的詳細(xì)的代碼示例可以看AngularJS的作者M(jìn)i?ko Hevery大神的一篇blog,其中詳細(xì)列舉了各種會(huì)影響單元測(cè)試的代碼范例票摇。

擴(kuò)展知識(shí)

Code Smells

如果覺(jué)得上面的各種原則還是有點(diǎn)抽象拘鞋,不好理解,那么直接學(xué)習(xí)Code Smells是一個(gè)很好的辦法矢门。

Code Smells 表示代碼中可能會(huì)導(dǎo)致潛在問(wèn)題的某些特征盆色。這里簡(jiǎn)單列舉一些典型的Code Smell灰蛙。

方法名過(guò)長(zhǎng),單個(gè)類(lèi)代碼過(guò)多隔躲,嗜好基本類(lèi)型摩梧,參數(shù)列表過(guò)長(zhǎng),無(wú)用的Field宣旱,等仅父。其實(shí)使用代碼檢查工具就可以有效減少Code Smells。

思考:為什么我們鐘愛(ài)依賴(lài)注入框架

因?yàn)橐蕾?lài)注入框架可以幫助我們更好的實(shí)現(xiàn)幾乎上述所有內(nèi)容浑吟,既能提高代碼質(zhì)量笙纤,又能輕松的編寫(xiě)單元測(cè)試,何樂(lè)而不為组力。

Reference:

http://wiki.c2.com/?LawOfDemeter

https://testing.googleblog.com/2008/08/by-miko-hevery-so-you-decided-to.html

https://testing.googleblog.com/2008/07/how-to-think-about-new-operator-with.html

https://testing.googleblog.com/2008/07/breaking-law-of-demeter-is-like-looking.html

https://youtu.be/pK7W5npkhho

http://misko.hevery.com/attachments/Guide-Writing%20Testable%20Code.pdf

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末省容,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子燎字,更是在濱河造成了極大的恐慌腥椒,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件候衍,死亡現(xiàn)場(chǎng)離奇詭異谎势,居然都是意外死亡播聪,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)杯瞻,“玉大人,你說(shuō)我怎么就攤上這事勇凭】忻悖” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵随闺,是天一觀(guān)的道長(zhǎng)日川。 經(jīng)常有香客問(wèn)我,道長(zhǎng)矩乐,這世上最難降的妖魔是什么龄句? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮散罕,結(jié)果婚禮上分歇,老公的妹妹穿的比我還像新娘。我一直安慰自己欧漱,他們只是感情好职抡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著误甚,像睡著了一般缚甩。 火紅的嫁衣襯著肌膚如雪谱净。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天擅威,我揣著相機(jī)與錄音壕探,去河邊找鬼。 笑死郊丛,一個(gè)胖子當(dāng)著我的面吹牛李请,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宾袜,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼捻艳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了庆猫?” 一聲冷哼從身側(cè)響起认轨,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎月培,沒(méi)想到半個(gè)月后嘁字,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡杉畜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年纪蜒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片此叠。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡纯续,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出灭袁,到底是詐尸還是另有隱情猬错,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布茸歧,位于F島的核電站倦炒,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏软瞎。R本人自食惡果不足惜逢唤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望涤浇。 院中可真熱鬧鳖藕,春花似錦、人聲如沸只锭。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至页滚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铺呵,已是汗流浹背裹驰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留片挂,地道東北人幻林。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像音念,于是被迫代替她去往敵國(guó)和親沪饺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容