本節書摘來自異步社群《android 源碼設計模式解析與實戰》一書中的第1章,第1.2節讓程式更穩定、更靈活——開閉原則,作者 何紅輝 , 關愛民,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視
1.2 讓程式更穩定、更靈活——開閉原則
開閉原則的英文全稱是open close principle,縮寫是ocp,它是java世界裡最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。開閉原則的定義是:軟體中的對象(類、子產品、函數等)應該對于擴充是開放的,但是,對于修改是封閉的。在軟體的生命周期内,因為變化、更新和維護等原因需要對軟體原有代碼進行修改時,可能會将錯誤引入原本已經經過測試的舊代碼中,破壞原有系統。是以,當軟體需要變化時,我們應該盡量通過擴充的方式來實作變化,而不是通過修改已有的代碼來實作。當然,在現實開發中,隻通過繼承的方式來更新、維護原有系統隻是一個理想化的願景,是以,在實際的開發過程中,修改原有代碼、擴充代碼往往是同時存在的。
軟體開發過程中,最不會變化的就是變化本身。産品需要不斷地更新、維護,沒有一個産品從第一版本開發完就再沒有變化了,除非在下個版本誕生之前它已經被終止。而産品需要更新,修改原來的代碼就可能會引發其他的問題。那麼,如何確定原有軟體子產品的正确性,以及盡量少地影響原有子產品,答案就是,盡量遵守本章講述的開閉原則。
勃蘭特·梅耶在1988年出版的《面向對象軟體構造》一書中提出這一原則——開閉原則。這一想法認為,程式一旦開發完成,程式中一個類的實作隻應該因錯誤而被修改,新的或者改變的特性應該通過建立不同的類實作,建立的類可以通過繼承的方式來重用原類的代碼。顯然,梅耶的定義提倡實作繼承,已存在的實作類對于修改是封閉的,但是新的實作類可以通過覆寫父類的接口應對變化。
說了這麼多,想必大家還是半懂不懂,還是讓我們以一個簡單示例說明一下。
在對imageloader進行了一次重構之後,小民的這個開源庫獲得了一些使用者,小民第一次感受成功的快樂,對開源的熱情也越發高漲起來!通過動手實作一些開源庫來深入學習相關技術,不僅能夠提升自我,也能更好地将這些技術運用到工作中,進而開發出更穩定、更優秀的應用,這就是小民的真實想法。
小民第一輪重構之後的imageloader職責單一、結構清晰,不僅獲得了主管的一點肯定,還得到了使用者的誇獎,算是個不錯的開始。随着使用者的增多,有些問題也暴露出來了,小民的緩存系統就是大家“吐槽”最多的地方。通過記憶體緩存解決了每次從網絡加載圖檔的問題,但是,android應用的記憶體很有限,且具有易失性,即當應用重新啟動之後,原來已經加載過的圖檔将會丢失,這樣重新開機之後就需要重新下載下傳!這又會導緻加載緩慢、耗費使用者流量的問題。小民考慮引入sd卡緩存,這樣下載下傳過的圖檔就會緩存到本地,即使重新開機應用也不需要重新下載下傳了。小民在和主管讨論了該問題之後就投入了程式設計中,下面就是小民的代碼。
從上述的代碼中可以看到,僅僅新增了一個diskcache類和往imageloader類中加入了少量代碼就添加了sd卡緩存的功能,使用者可以通過usediskcache方法來對使用哪種緩存進行設定,例如:
通過usediskcache方法可以讓使用者設定不同的緩存,非常友善啊!小民對此很滿意,于是送出給主管做代碼稽核。“小民,你思路是對的,但是有些明顯的問題,就是使用記憶體緩存時使用者就不能使用sd卡緩存。類似地,使用sd卡緩存時使用者就不能使用記憶體緩存。使用者需要這兩種政策的綜合,首先緩存優先使用記憶體緩存,如果記憶體緩存沒有圖檔再使用sd卡緩存,如果sd卡中也沒有圖檔最後才從網絡上擷取,這才是最好的緩存政策。”主管的解釋真是一針見血,小民這時才如夢初醒,剛才還得意洋洋的臉上突然有些泛紅……
于是小民按照主管的指點建立了一個雙緩存類doublecache,具體代碼如下:
通過增加短短幾行代碼和幾處修改就完成了如此重要的功能。小民已越發覺得自己android開發已經到了得心應手的境地,頓時感覺一陣春風襲來,小民感覺今天天空比往常敞亮許多。
“小民,你每次加新的緩存方法時都要修改原來的代碼,這樣很可能會引入bug,而且會使原來的代碼邏輯變得越來越複雜。按照你這樣的方法實作,使用者也不能自定義緩存實作呀!”到底是主管水準高,一語道出了小民這緩存設計上的問題。
我們還是來分析一下小民的程式。小民每次在程式中加入新的緩存實作時都需要修改imageloader類,然後通過一個布爾變量來讓使用者選擇使用哪種緩存,是以,就使得在imageloader中存在各種if-else判斷語句,通過這些判斷來确定使用哪種緩存。随着這些邏輯的引入,代碼變得越來越複雜、脆弱,如果小民一不小心寫錯了某個if條件(條件太多,這是很容易出現的),那就需要更多的時間來排除,整個imageloader類也會變得越來越臃腫。最重要的是,使用者不能自己實作緩存注入到imageloader中,可擴充性差,可擴充性可是架構的最重要特性之一。
“軟體中的對象(類、子產品、函數等)應該對于擴充是開放的,但是對于修改是封閉的,這就是開放——關閉原則。也就是說,當軟體需要變化時,我們應該盡量通過擴充的方式來實作變化,而不是通過修改已有的代碼來實作。”小民的主管補充到,小民聽得雲裡霧裡的。主管看小民這等反應,于是親自“操刀”,為他畫下了圖1-2所示的uml圖。
小民看到圖1-2似乎明白些什麼,但又不是很明确如何修改程式。主管看到小民這般模樣,隻好親自上陣,帶着小民把imageloader程式按照圖1-2進行了一次重構,具體代碼如下:
經過這次重構,沒有了那麼多的if-else語句,沒有了各種各樣的緩存實作對象、布爾變量,代碼确實清晰、簡單了很多,小民對主管的崇敬之情又“泛濫”了起來。需要注意的是,這裡的imagecache類并不是小民原來的那個imagecache,這次重構程式,主管把它提取成一個圖檔緩存的接口,用來抽象圖檔緩存的功能,我們看看該接口的聲明:
imagecache接口簡單定義了擷取、緩存圖檔兩個函數,緩存的key是圖檔的url,值是圖檔本身。記憶體緩存、sd卡緩存、雙緩存都實作了該接口,我們看看這幾個緩存實作:
細心的讀者可能注意到了,imageloader類中增加了一個etimagecache(imagecache cache)函數,使用者可以通過該函數設定緩存實作,也就是通常說的依賴注入。下面就看看使用者是如何設定緩存實作的:
在上述代碼中,通過setimagecache(imagecache cache)方法注入不同的緩存實作,這樣不僅能夠使imageloader更簡單、健壯,也使得imageloader的可擴充性、靈活性更高。memorycache、diskcache、doublecache緩存圖檔的具體實作完全不一樣,但是,它們的一個特點是,都實作了imagecache接口。當使用者需要自定義實作緩存政策時,隻需要建立一個實作imagecache接口的類,然後構造該類的對象,并且通過setimagecache(imagecache cache)注入到imageloader中,這樣imageloader就實作了千變萬化的緩存政策,且擴充這些緩存政策并不會導緻imageloader類的修改。經過這次重構,小民的imageloader已經基本算合格了。咦!這不就是主管說的開閉原則麼!“軟體中的對象(類、子產品、函數等)應該對于擴充是開放的,但是,對于修改是封閉的。而遵循開閉原則的重要手段應該是通過抽象……”小民細聲細語地念叨着,陷入了思索中……
開閉原則指導我們,當軟體需要變化時,應該盡量通過擴充的方式來實作變化,而不是通過修改已有的代碼來實作。這裡的“應該盡量”4個字說明ocp原則并不是說絕對不可以修改原始類的。當我們嗅到原來的代碼“腐化氣味”時,應該盡早地重構,以便使代碼恢複到正常的“進化”過程,而不是通過繼承等方式添加新的實作,這會導緻類型的膨脹以及曆史遺留代碼的備援。我們的開發過程中也沒有那麼理想化的狀況,完全地不用修改原來的代碼,是以,在開發過程中需要自己結合具體情況進行考量,是通過修改舊代碼還是通過繼承使得軟體系統更穩定、更靈活,在保證去除“代碼腐化”的同時,也保證原有子產品的正确性。