蘇州工業(yè)園區(qū)勞動局網(wǎng)站做不了合同長春seo網(wǎng)站排名
概述
對前面的內(nèi)容的回顧,溫故而知新,包括:面向?qū)ο?、設(shè)計原則、規(guī)范與重構(gòu)三個模塊的內(nèi)容。
1.代碼質(zhì)量評判標(biāo)準(zhǔn)
如何評價代碼質(zhì)量的高低?
代碼質(zhì)量的評價具有很強的主觀性,描述代碼質(zhì)量的詞匯也有很多,比如可讀性、可維護性、靈活、優(yōu)雅、簡潔。這些詞匯是從不同的維度去評價代碼質(zhì)量的。它們之間有相互作用,并不是獨立的,比如,代碼的可讀性好、可擴展性好就意味著代碼的可維護性好。代碼質(zhì)量高低是一個綜合各種因素得到的結(jié)論。我們并不能通過單一維度去評價一段代碼的好壞。
最常用的評價標(biāo)準(zhǔn)有哪幾個?
最常用到幾個評判代碼質(zhì)量的標(biāo)準(zhǔn)有:可維護性、可讀性、可擴展性、靈活性、簡潔性、可復(fù)用性、可測試性。其中,可維護性、可讀性、可擴展性又是提到最多、最重要的三個評價標(biāo)準(zhǔn)。
如何才能寫出高質(zhì)量的代碼?
要寫出高質(zhì)量的代碼,我們就需要掌握一些更加細化、更加能落地的編程方法論,這就包含面向?qū)ο笤O(shè)計思想、設(shè)計原則、設(shè)計模式、編碼規(guī)范、重構(gòu)技巧等。
面向?qū)ο?/h2>
1.面向?qū)ο蟾攀?/h3>
現(xiàn)在,主流的編程范式或者編程風(fēng)格有三種,它們分別是面向過程、面向?qū)ο蠛秃瘮?shù)式編程。面向?qū)ο筮@種編程風(fēng)格又是這其中最主流的?,F(xiàn)在比較流行的編程語言大部分是面向?qū)ο缶幊陶Z言。大部分項目也是基于面向?qū)ο缶幊田L(fēng)格開發(fā)的。面向?qū)ο缶幊桃驗槠渚哂胸S富的特性(封裝、抽象、繼承、多態(tài)),可以實現(xiàn)很多復(fù)雜的設(shè)計思路,是很多設(shè)計原則、設(shè)計模式編碼實現(xiàn)的基礎(chǔ)。
2.面向?qū)ο笏拇筇匦?/h3>
封裝也叫作信息隱藏和數(shù)據(jù)訪問保護。類通過暴露有限的訪問接口,授權(quán)外部僅能通過類提供的方法來訪問內(nèi)部信息或者數(shù)據(jù)。它需要編程語言提供權(quán)限訪問控制語法來支持,例如 Java 中的 private、protected、public 關(guān)鍵字。封裝特性存在的意義,一方面是保護數(shù)據(jù)不被隨意修改,提高代碼的可維護性;另一方面是僅暴露有效的必要接口,提高類的易用性。
如果說封裝是講的是如何隱藏信息,那抽象就是將如何隱藏方法的具體實現(xiàn),讓使用者只需要關(guān)心方法提供了哪些功能,不需要知道這些功能是如何實現(xiàn)的。抽象可以通過接口或者抽象類來實現(xiàn)。抽象存在的意義,一方面是修改實現(xiàn)類不需要改變定義;另一方面,它也是處理復(fù)雜系統(tǒng)的有效手段,能有效地過濾不必要關(guān)注的信息。
繼承用來表示 is-a 的關(guān)系,分為兩種模式:單根繼承和多繼承。單繼承表示一個子類只能繼承一個父類,多繼承表示一個子類可以繼承多個父類。為了實現(xiàn)繼承這個特性,編程語言需要提供特殊的語法機制來支持。繼承主要用來解決代碼復(fù)用的問題。
多態(tài)是指子類替換父類,在實際的代碼運行過程中,調(diào)用子類的方法實現(xiàn)。多態(tài)這種特性也需要編程語言提供特殊的語法機制來實現(xiàn),比如繼承、接口類、duck-typing。多態(tài)可以提高代碼的可擴展性和復(fù)用性,是很多設(shè)計模式、設(shè)計原則、編程技巧大代碼實現(xiàn)基礎(chǔ)。
3.面向?qū)ο骎S面向過程
面向?qū)ο笙啾让嫦蜻^程編程的優(yōu)勢:
- 對于大規(guī)模復(fù)雜程序的開發(fā),程序處理流程并非單一的一條主線,而是錯綜復(fù)雜的網(wǎng)狀結(jié)構(gòu)。面向?qū)ο缶幊瘫绕鹈嫦蜻^程編程,更能應(yīng)對這種復(fù)雜類型的程序開發(fā)。
- 面向?qū)ο缶幊滔啾让嫦蜻^程編程,具有更加豐富的特性(封裝、抽象、繼承、多態(tài))。利用這些特性編寫出來的代碼,更加易擴展、易維護、易復(fù)用。
- 從編程語言跟機器打交道方式的演進規(guī)律中,可以總結(jié)出:面向?qū)ο缶幊瘫绕鹈嫦蜻^程編程,更加人性化、更加高級、更加智能。
面向?qū)ο缶幊桃话闶褂妹嫦驅(qū)ο缶幊陶Z言來進行,但是不用面向?qū)ο缶幊陶Z言,我們照樣可以進行面向?qū)ο缶幊?。反過來講,即便我們使用面向?qū)ο缶幊陶Z言,寫出來的代碼也不一定是面向?qū)ο缶幊田L(fēng)格的,也可能是面向過程編程風(fēng)格的。
面向?qū)ο蠛兔嫦蜻^程兩種編程風(fēng)格不是完全對立的。在用面向?qū)ο缶幊陶Z言開發(fā)的軟件中,面向過程風(fēng)格的代碼并不少見,甚至在一些標(biāo)準(zhǔn)的開發(fā)庫(如 JDK、Apache Commons、Google Guava)中,也有很多面向過程風(fēng)格的代碼。
不管使用面向過程還是面向?qū)ο髞韺懘a,最終的目的還是寫出易維護、易讀、易復(fù)用、易擴展的高質(zhì)量代碼。只要我們能避免面向過程編程風(fēng)格的一些弊端,控制好它的副作用,在掌控范圍內(nèi)為我們所用,我們就大可不用避諱在面向?qū)ο缶幊讨袝鴮懨嫦蜻^程風(fēng)格的代碼。
4.面向?qū)ο蠓治?、設(shè)計與編程
面向?qū)ο蠓治?#xff08;OOA)、面向?qū)ο笤O(shè)計(OOD)、面向?qū)ο缶幊?#xff08;OOP),是面向?qū)ο箝_發(fā)的三個主要環(huán)節(jié)。
- 面向?qū)ο蠓治鼍褪且闱宄鍪裁?/li>
- 面向?qū)ο笤O(shè)計就是要搞清楚怎么做
- 面向?qū)ο缶幊叹褪菍⒎治龊驮O(shè)計的結(jié)果翻譯成代碼的過程
需求分析的過程實際上是一個不斷迭代優(yōu)化的過程。我們不要視圖一下就給出一個完美的解決方案,而是先給出一個粗糙的、基礎(chǔ)的方案,有一個迭代的基礎(chǔ),然后再慢慢優(yōu)化。這樣一個思考過程能讓我們擺脫無從下手的窘境。
面向?qū)ο笤O(shè)計和實現(xiàn)要做的事情就是把合適的代碼放到合適的類中。至于到底選擇哪種劃分方法,判定的標(biāo)準(zhǔn)是讓代碼盡量滿足 “松耦合、高內(nèi)聚”、單一職責(zé)、對擴展開發(fā)對修改關(guān)閉等我們之前講到的各種設(shè)計思想和原則,盡量地做到代碼可復(fù)用、易讀、易擴展、易維護。
面向?qū)ο蠓治龅漠a(chǎn)出是詳細的需求描述。面向?qū)ο笤O(shè)計的產(chǎn)出是類。在面向?qū)ο笤O(shè)計這一環(huán)節(jié),我們將需求轉(zhuǎn)化為具體的類的設(shè)計。這個環(huán)節(jié)的工作可以拆分為下面四個部分。
- 劃分職責(zé)進而識別出有哪些類。
根據(jù)需求描述,我們把其中涉及的功能點,一個個羅列出來,然后再去看哪些功能點職責(zé)相近,操作同樣的屬性,可否歸為同一個類。 - 定義類的屬性和方法。
識別出需求描述中的動詞,作為候選方法,再進一步過濾篩選出真正的方法。把功能點涉及的名詞作為候選屬性,然后再同樣進行過濾篩選。 - 定義類與類之間的交互關(guān)系
UML 統(tǒng)一建模語言定義了六種類之間的關(guān)系。分別是:泛華、實現(xiàn)、關(guān)聯(lián)、組合、聚合、依賴。我們從更加貼近編程的角度,對類與類之間的關(guān)系做了調(diào)整,保留了四個關(guān)系:泛華、實現(xiàn)、組合、依賴。 - 將類組裝起來并提供執(zhí)行入口
將所有的類組裝在一起,提供一個執(zhí)行入口。這個入口可能是一個 main() 函數(shù),也可能是一個組合外部用的 API 接口。通過這個入口,可以出發(fā)整個代碼跑起來。
5.接口VS抽象類
抽象類不允許被實例化,只能被繼承。它可以包含屬性和方法。方法既可以包含代碼實現(xiàn),也可以不包含代碼實現(xiàn)。不包含代碼實現(xiàn)的方法叫做抽象方法。子類繼承父類必須實現(xiàn)抽象類中的所有抽象方法。
接口不能包含屬性(Java 可以定義靜態(tài)常量),只能申明方法,方法不能包含代碼實現(xiàn)(Java8 以后可以有默認實現(xiàn))。類實現(xiàn)接口的時候,必須實現(xiàn)接口中申明的所有方法。
抽象類是對成員變量和方法的抽象,是一種 is-a 的關(guān)系,是為了解決代碼復(fù)用問題。接口僅僅是對方法的抽象,是一種 has-a 的關(guān)系,表示具有一組行為特性,是為了解決解耦問題,隔離接口和具體的實現(xiàn),提高代碼的擴展性。
什么實時使用抽象類?什么時候使用接口?
實際上,判斷的標(biāo)準(zhǔn)很簡單。如果要表示 is-a 的關(guān)系,并且是為了解決代碼復(fù)用問題,就用抽象類。如果要表示一種 has-a 的關(guān)系,并且是為了解決抽象而非代碼復(fù)用的問題,那我們就用接口。
6.基于接口而非實現(xiàn)編程
應(yīng)用這條原則,可以將接口和實現(xiàn)相分離,封裝不穩(wěn)定的實現(xiàn),暴露穩(wěn)定的接口。上游系統(tǒng)面向接口而非實現(xiàn)編程,不依賴不穩(wěn)定的實現(xiàn)細節(jié),這樣當(dāng)實現(xiàn)發(fā)生變化的時候,上游系統(tǒng)的代碼基本上不需要做改動,以此來降低耦合性,提高擴展性。
實際上,“基于接口而非實現(xiàn)編程” 這條原則的另一個表述方式是,“基于抽象而非實現(xiàn)編程”。后者的表述方式其實更能體現(xiàn)這條設(shè)計原則的設(shè)計初衷。在軟件開發(fā)中,最大的挑戰(zhàn)之一就是需求的不斷變化,這也是考驗代碼設(shè)計好壞的一個標(biāo)準(zhǔn)。
越抽象、越頂層、越脫離具體某一實現(xiàn)的設(shè)計,越能提高代碼的靈活性,越能應(yīng)對未來的需求變化。好的代碼設(shè)計,不能能應(yīng)對當(dāng)下的需求,而且在將來需求發(fā)生變化的時候,仍然能在不破壞原有代碼設(shè)計的情況下靈活應(yīng)對。而抽象就是提供代碼擴展性、靈活性、可維護性最有效的手段之一。
7.多用組合少用繼承
為什么不推薦使用繼承
繼承是面向?qū)ο蟮乃拇筇匦灾?#xff0c;用來表示 is-a 關(guān)系,可以解決代碼復(fù)用的問題。雖然繼承有諸多作用,但繼承層次過深、過復(fù)雜,也會影響到代碼的可維護性。在這種情況下,我們應(yīng)該盡量少用,甚至不用繼承。
組合相比繼承有哪些優(yōu)勢?
繼承的主要作用有三個:表示 is-a 關(guān)系、支持多態(tài)特性、代碼復(fù)用。而這三個作用都可以通過組合、接口、委托三個技術(shù)手段來達成。此外,利用組合還能解決層次過深、過復(fù)雜的繼承關(guān)系影響代碼可維護性的問題。
如何判斷該用組合還是繼承?
盡管我們鼓勵多用組合少用繼承,但組合也并不是完美的,繼承也并非一無是處。在實際的項目開發(fā)中,我們還是要根據(jù)具體的情況,來選擇該用繼承還是組合。
- 如果類之間的結(jié)構(gòu)穩(wěn)定,層次比較淺,關(guān)系不復(fù)雜,我們就可以大膽地使用繼承。
- 反之,我們就盡量使用組合來替代繼承。
- 還有一些設(shè)計模式、特殊的應(yīng)用常見,會固定使用組合或者繼承。
8.貧血模型 VS 充血模型
我們平時做 WEB 項目的業(yè)務(wù),大部分都是基于貧血模型的 MVC 三層架構(gòu),被稱為傳統(tǒng)開發(fā)模式。之所以稱之為 “傳統(tǒng)”,是相對于新興的基于充血模型的 DDD 開發(fā)模式來說的?;谪氀P偷膫鹘y(tǒng)開發(fā)模式,是典型的面向過程的編程風(fēng)格。相反,基于充血模型的 DDD 開發(fā)模式,是典型的面向?qū)ο蟮木幊田L(fēng)格。
不過,DDD 也并非銀彈。對于業(yè)務(wù)不復(fù)雜的系統(tǒng)開發(fā)來說,基于貧血模型的傳統(tǒng)開發(fā)模式簡單夠用,基于充血模型的 DDD 開發(fā)模式有點大材小用,無法發(fā)揮作用。相反,對于業(yè)務(wù)負責(zé)的系統(tǒng)開發(fā)來說,基于充血模型的 DDD 開發(fā)模式,因為前期需要在設(shè)計上投入更多的時間和經(jīng)歷,來提高代碼的復(fù)用性和可維護性,所以相比于基于貧血模型的開發(fā)模式,更加有優(yōu)勢。
基于充血模型的 DDD 開發(fā)模式跟基于貧血模型的傳統(tǒng)開發(fā)模式相比,主要區(qū)別在 Service 層。在基于充血模型的開發(fā)模式下,我們將部分原來在 Service 類中的業(yè)務(wù)邏輯移動到了一個充血的 domain 領(lǐng)域模型中,讓 Service 類的實現(xiàn)依賴這個 domain 類。不過,Service 類并不會完全移除,而是復(fù)雜一些不適合放在 domain 領(lǐng)域模型類中的功能。比如,負責(zé)與 Repository 層打交道、跨領(lǐng)域模型的業(yè)務(wù)聚合功能、冪等事務(wù)等非功能性的工作。
基于充血模型的 DDD 開發(fā)模式跟基于貧血模型的傳統(tǒng)開發(fā)模式相比,Controller 層和 Repository 層的代碼基本上相同。這是因為,Repository 層的 Entity 生命周期有限,Controller 層的 VO 只是單純作為一種 DTO。兩部分的業(yè)務(wù)邏輯都不會太復(fù)雜。業(yè)務(wù)邏輯主要集中在 Service 層。所以,Controller 層和 Repository 層繼續(xù)沿用貧血模型的設(shè)計思路是沒有問題的。
設(shè)計原則
1.SOLID 原則:SRP 單一職責(zé)原則
一個類只負責(zé)完成一個職責(zé)或者功能。單一職責(zé)原則通過避免設(shè)計大而全的類,避免將不相關(guān)的功能耦合在一起,來提高類的內(nèi)聚性。同時,類職責(zé)單一,類依賴的和被依賴的其他類也會變少,減少了代碼的耦合性,以此來實現(xiàn)代碼的高內(nèi)聚、松耦合。但是,如果拆分得過細,實際上會適得其反,反倒會降低內(nèi)聚性,也會影響代碼的可維護性。
不同的應(yīng)用常見、不同階段的需求背景、不同的業(yè)務(wù)層面,對同一個類的職責(zé)是否單一,可能會有不同的判定結(jié)果。實際上,一些側(cè)面的判斷指標(biāo)更具有指導(dǎo)意義和可執(zhí)行性,比如,出現(xiàn)下面這些情況就有可能說明類的設(shè)計不滿足單一職責(zé)原則:
- 類中的代碼行數(shù)、函數(shù)或者屬性過多;
- 類依賴的其他類過多,或者依賴類的其他類過多;
- 私有方法過多;
- 比較難給類起一個合適的名字;
- 類中大量的方法都是集中操作類中的幾個屬性。
2.SOLID 原則:OCP 開閉原則
如何理解 “對擴展開發(fā)、對修改關(guān)閉”?
添加一個新的功能,應(yīng)該是通過在已有代碼基礎(chǔ)上擴展代碼(新增模塊、類、方法、屬性等),而非修改代碼已有代碼(修改模塊、類、方法、屬性等)的方式來完成。關(guān)于定義,我們有兩點要注意。
- 第一點是,開閉原則并不是說完全杜絕修改,而是以最小的修改代碼的代價來完成新功能的開發(fā)。
- 第二點是,同樣代碼的改動,在粗代碼粒度下,可能被認為是 “修改”;在細代碼粒度下,可能有被認為 “擴展”;
如何做到 “對擴展開發(fā)、對修改關(guān)閉”?
我們要時刻具備擴展意識、抽象意識、封裝意識。在寫代碼時,我們要多花點時間思考下,這段代碼未來可能有哪些需求變更,如何設(shè)計代碼結(jié)構(gòu),事先留好擴展點,以便在未來需求變更時,在不改動代碼整體結(jié)構(gòu)、做到最小代碼改動的情況下,將新的代碼靈活地插入到擴展點上。
很多設(shè)計原則、設(shè)計思想、設(shè)計模式,都是以提高代碼的擴展性為最終目的。特別是 23 種經(jīng)典設(shè)計模式,大部分都是為了解決代碼的擴展性問題而總結(jié)出來的,都是以開閉原則為指導(dǎo)原則的。最常用來提高代碼可擴展性的方法有:多態(tài)、依賴注入、基于接口而非實現(xiàn)編程,以及大部分的設(shè)計模式(比如,裝飾、策略、模板、職責(zé)鏈、狀態(tài))。
3.SOLID 原則:LSP 里氏替換原則
子類對象(Object of subtype/derived class)能夠替換程序中父類對象出現(xiàn)的任何地方,并且保證原來程序的邏輯行為不變及正確性不被破壞。
里氏替換原則是用來指導(dǎo)繼承關(guān)系中子類該如何設(shè)計的一個原則。理解里氏替換原則,最核心的就是理解 “design by contract,按照協(xié)議來設(shè)計” 這幾個字。父類定義了函數(shù)的 “約定”,子類可以改變函數(shù)的內(nèi)部實現(xiàn)邏輯,但不能改變函數(shù)的原有 “約定” 包括:函數(shù)聲明要實現(xiàn)的功能;對輸入、輸出、異常的約定;甚至包括注釋中所羅列的任何特殊說明。
理解這個原則,我們還要弄明白,里氏替換原則跟多態(tài)的區(qū)別。雖然從定義描述和代碼實現(xiàn)上來看,多態(tài)和里氏替換原則有點類似,但它們關(guān)注的角度是不一樣的。多態(tài)是面向?qū)ο缶幊痰囊淮筇匦?#xff0c;也是面向?qū)ο缶幊陶Z言的一種語法。它是一種代碼實現(xiàn)的思路。而里氏替換原則是一種設(shè)計原則,用來指導(dǎo)繼承關(guān)系中子類該如何設(shè)計,子類的設(shè)計要保證在替換父類的時候,不改變原有程序的邏輯及不破壞原有程序的正確性。
4.SOLID 原則:ISP 接口隔離原則
接口隔離原則的描述是:客戶端不應(yīng)該強迫依賴它不需要的接口。其中的 “客戶端”,可以理解為接口的調(diào)用者。理解 “接口隔離原則” 的重點是理解其中的 “接口” 二字。這里有三種不同的理解。
如果把 “接口” 理解為一組集合,可以是某個微服務(wù)的接口,也可以是某個類庫的接口等。如果部分接口只被部分調(diào)用者使用,我們就需要將這部分接口隔離出來,單獨給這部分調(diào)用者使用,而不強迫其他調(diào)用者也依賴這部分不會被用到的接口。
如果把 “接口” 理解為單個 API 接口或函數(shù),部分調(diào)用者只需要函數(shù)中的部分功能,那我們就需要把函數(shù)拆分成粒度更細的多個函數(shù),讓調(diào)用者只依賴它需要的那個細粒度函數(shù)。
如果把 “接口” 理解為 OOP 中的接口,也可以理解為面向?qū)ο缶幊陶Z言中的接口語法。那接口的設(shè)計要盡量單一,不要讓接口的實現(xiàn)類和調(diào)用者,依賴不需要的接口函數(shù)。
5.SOLID 原則:DIP 依賴倒置原則
**控制反轉(zhuǎn):**實際上,控制反轉(zhuǎn)是一個比較籠統(tǒng)的設(shè)計思想,并不是一種具體的實現(xiàn)方法,一般用來指導(dǎo)框架層面的設(shè)計。這里所說的 “控制” 指的是對執(zhí)行流程的控制,而 “反轉(zhuǎn)” 指的是在沒有使用框架之前,程序員自己控制整個程序的執(zhí)行。在使用框架之后,整個程序的執(zhí)行流程通過框架來控制。流程的控制權(quán)從程序員 “反轉(zhuǎn)” 給了框架。
**依賴注入:**依賴注入和控制反轉(zhuǎn)恰恰相反,它是一種具體的編程技巧。我們不通過 new 的方式在類內(nèi)部創(chuàng)建依賴的對象,而是將依賴類對象在外部創(chuàng)建好之后,通過構(gòu)造函數(shù)、函數(shù)參數(shù)等方式傳遞(或 “注入”)給類來使用。
**依賴注入框架:**通過依賴注入框架提供的擴展點,簡單配置一下所需要的類及其類之間的依賴關(guān)系,就可以實現(xiàn)由框架來自動創(chuàng)建對象、管理對象生命周期、依賴注入等原本需要程序員來做的事情。。
**依賴反轉(zhuǎn)原則:**也叫做依賴倒置原則。這條原則跟控制反轉(zhuǎn)有點類似,主要用來指導(dǎo)框架層面的設(shè)計。高層模塊不依賴低層每塊,它們依賴一個共同抽象。抽象不需要依賴具體實現(xiàn)細節(jié),具體實現(xiàn)細節(jié)依賴抽象。
6.KISS、YAGNI 原則
KISS 原則的中文描述是:盡量保持簡單。KISS 原則是保持代碼可讀性和可維護性的重要手段。KISS 原則中的 “簡單” 并不是以代碼行數(shù)來考量的。代碼行數(shù)越少并不是代表代碼越簡單,我們還要考慮邏輯復(fù)雜度、實現(xiàn)難度、代碼的可讀性等。而且,本身就復(fù)雜的問題,用復(fù)雜的方法解決,也并不違背 KISS 原則。此外,同樣的代碼,在某個業(yè)務(wù)場景下不滿足 KISS 原則,換一個應(yīng)用場景可能就不滿足了。
對于如何寫出滿足 KISS 原則的代碼,總結(jié)了下面幾條指導(dǎo)原則:
- 不要使用同事坑你不懂的技術(shù)來實現(xiàn)代碼;
- 不要重復(fù)造輪子,善于使用已經(jīng)有的工具類庫;
- 不要過度優(yōu)化;
YAGNI 原則的英文全稱是:You Ain’t Gonna Need It。直譯就是:你不需要它。這條原則也算是萬金油了。當(dāng)用在軟件開發(fā)中時,它的意思是:不要去設(shè)計用不到的功能,不要是編寫當(dāng)前用不到的代碼。實際上,這條原則的核心思想是:不要過度設(shè)計。
YAGNI 原則跟 KISS 原則并非一回事兒。KISS 原則講的是 “如何做” 的問題(盡量保持簡單),而 YAGNI 原則說的是 “要不要做” 的問題(當(dāng)前不需要的就不要做)。
7.DRY原則
DRY 原則中文描述是:不要重復(fù)自己,將它應(yīng)用在編程中,可以理解為:不要寫重復(fù)的代碼。
有三種代碼重復(fù)的情況:實現(xiàn)邏輯重復(fù)、功能語義重復(fù)、代碼執(zhí)行重復(fù)。實現(xiàn)邏輯重復(fù),但語義功能不重復(fù)的代碼,并不違反 DRY 原則。實現(xiàn)邏輯不重復(fù),但功能語義重復(fù)的代碼,也算違反 DRY 原則。而執(zhí)行代碼重復(fù)的代碼,也算違反 DRY 原則。
此外,我們還降到了提高代碼復(fù)用性的一些手段,包括:減少代碼耦合、滿足單一職責(zé)原則、模塊化、業(yè)務(wù)與非業(yè)務(wù)邏輯分離、通用代碼下沉、繼承、多態(tài)、封裝、抽象、應(yīng)用模板等設(shè)計模式。
復(fù)用意識也非常重要。在設(shè)計每個模塊、類、函數(shù)的時候,要像設(shè)計一個外部 API 一樣去思考它的復(fù)用性。
我們在第一次寫代碼的時候,如果當(dāng)下沒有復(fù)用的需求,而未來的復(fù)用需求也不是特別明確,并且開發(fā)可復(fù)用代碼的成本比較高,那我們就不需要考慮代碼的復(fù)用性。在之后開發(fā)新的功能的時候,發(fā)現(xiàn)可以復(fù)用之前寫的這段代碼,那我們就重構(gòu)這段代碼,讓其變得可復(fù)用。
相比于代碼的復(fù)用,DRY 原則適用性更強一些。我們可以不寫可復(fù)用的代碼,但一定不能寫重復(fù)的代碼、
8.LOD原則
如何理解 “高內(nèi)聚、松耦合”?
“高內(nèi)聚、松耦合” 是一個非常重要的設(shè)計思想,能夠有效提高代碼的可讀性和可維護性,縮小功能改動導(dǎo)致的代碼改動范圍?!案邇?nèi)聚” 用來指導(dǎo)類與類之間依賴關(guān)系的設(shè)計。所謂高內(nèi)聚,就是指相近的功能應(yīng)該放到同一個類中,不相近的功能不要放到同一個類中。相近的功能往往會被同時修改,放到同一個類中,修改會比較集中。所謂 “松耦合” 指的是,在代碼中,類與類之間的依賴關(guān)系簡單清晰。即使兩個類有依賴關(guān)系,一個代碼的改動也不會或很少導(dǎo)致依賴類的代碼改動。
如何理解 “迪米特法則”?
迪米特法則的描述為:不該有直接依賴關(guān)系的類,不要有依賴;有依賴關(guān)系的類之間,盡量只依賴必要的接口。迪米特法則是希望較少類之間的耦合,讓類越獨立越好。每個類都應(yīng)該少了解系統(tǒng)的其他部分。一旦發(fā)生變化,需要了解這一變化的類就會比較少。
規(guī)范與重構(gòu)
1.重構(gòu)概述
重構(gòu)的目的:為什么重構(gòu)(why)?
對于項目來說,重構(gòu)可以保持代碼質(zhì)量持續(xù)處于一個可控狀態(tài),不至于腐化到無可救藥的地步。對于個人而言,重構(gòu)非常鍛煉一個人的代碼能力,并且是一件非常有成就感的事情。它是我們學(xué)習(xí)經(jīng)典設(shè)計思想、原則、模式、編程規(guī)范等理論知識的練兵場。
重構(gòu)的對象:重構(gòu)什么(what)?
按照重構(gòu)的規(guī)模,可以將重構(gòu)分為大規(guī)模高層次的重構(gòu)和小規(guī)模低層次的重構(gòu)。
- 大規(guī)模高層次重構(gòu)包括對代碼分層、模塊化、解耦、梳理類之間的交互關(guān)系、抽象復(fù)用組件等等。這部分工作利用的更多的是比較抽象、比較頂層的設(shè)計思想、原則、模式。
- 小規(guī)模低層次重構(gòu)包括命名規(guī)范、注釋、修正函數(shù)參數(shù)過多、消除超大類、提取重復(fù)代碼等編程細節(jié),主要是針對類、函數(shù)級別的重構(gòu)。小規(guī)模低層次重構(gòu)更多的是利用編碼規(guī)范這一理論知識。
重構(gòu)的時機:什么時候重構(gòu)(when)?
一定要建立持續(xù)重構(gòu)的意識,把重構(gòu)作為開發(fā)不可少的部分融入到開發(fā)中,而不是等到代碼出現(xiàn)很大問題的時候,再大刀闊斧地重構(gòu)。
重構(gòu)的方法:如何重構(gòu)(how)?
大規(guī)模高層次重構(gòu)難度比較大,需要有組織、有計劃地進行,分階段小步快跑,時刻保持代碼處于一個可運行的狀態(tài)。
小規(guī)模低層次重構(gòu),因為影響范圍小,改動耗時短,所以,只要你愿意并且有時間,隨時隨地都可以去做。
2.單元測試
什么是單元測試?
單元測試是代碼層面的測試,用于測試 “自己” 編寫的代碼的邏輯正確性。單元測試顧名思義是測試一個 “單元”,這個 “單元” 一般是類或函數(shù),而不是系統(tǒng)或模塊。
為什么要寫單元測試?
單元測試能有效地發(fā)現(xiàn)代碼中的 bug、代碼設(shè)計上的問題。寫單元測試的過程本身就是代碼重構(gòu)的過程。單元測試是對集成測試的有力補充,能幫助我們快速熟悉代碼,是 TDD 可落地執(zhí)行的折中方案。
如何編寫單元測試?
寫單元測試就是針對代碼設(shè)計覆蓋各種輸入、異常、邊界條件的測試用例,并將其翻譯成代碼的過程。可以利用一些測試框架來簡化測試代碼的編寫。對于單元測試,我們需要建立以下正確的認知:
- 編寫單元測試盡管繁瑣,但并不是太耗時;
- 可以稍微放低單元測試的質(zhì)量要求;
- 覆蓋率作為衡量單元測試好壞的唯一標(biāo)準(zhǔn)是不合理的;
- 寫單元測試一般不需要了解代碼的實現(xiàn)邏輯;
- 單元測試框架無法測試多半是代碼的可測試性不好。
單元測試為何難落地執(zhí)行?
- 一方面,寫單元測試本身比較繁瑣,技術(shù)挑戰(zhàn)不大,很多程序員不愿意去寫。
- 另一方面,國內(nèi)研發(fā)比較偏向 “快、糙、猛”,容易因為開發(fā)進度緊,導(dǎo)致單元測試的執(zhí)行虎頭蛇尾,最后,沒有建立單元測試的正確認識,覺得可有可無,單靠督促很難執(zhí)行得很好。
3.代碼的可測試性
什么是代碼的可測試性
粗略的講,所謂的代碼的可測試性,就是針對代碼編寫單元測試的難易程度。
對于一段代碼,如果很難為其編寫單元測試,或者單元測試寫起來很費勁,需要依靠單元測試框架很高級的特性,那往往就意味著代碼設(shè)計得不夠合理,代碼的可測試性不好。
編寫可測試性代碼的最有效手段
依賴注入是編寫可測試性代碼的最有效手段。通過依賴注入,我們在編寫單元測試代碼的時候,可以通過 mock 的方法將不可控的依賴變得可控,這也是我們在編寫單元測試的過程中最有技術(shù)挑戰(zhàn)的地方。除了 mock 方式,我們還可以利用二次封裝來解決某些代碼行為不可控的情況。
場景的 Anti-Patterns
典型的、常見的測試不友好的代碼有下面這 5 種:
- 代碼中包含未決行為邏輯;
- 濫用全局變量;
- 濫用靜態(tài)方法;
- 使用復(fù)雜的繼承關(guān)系;
- 高度耦合的代碼。
4.大型重構(gòu):解耦
“解耦” 為何如此重要?
過于復(fù)雜的代碼往往在可讀性、可維護性上都不友好。解耦,保證代碼松耦合、高內(nèi)聚,是控制代碼復(fù)雜度的有效手段。如果代碼松耦合、高內(nèi)聚,也就意味著,代碼結(jié)構(gòu)清晰、分層、模塊化合理、依賴關(guān)系簡單、模塊或類之間的耦合小,那代碼整體的質(zhì)量就不會差。
代碼是否需要 “解耦” ?
間接的衡量標(biāo)準(zhǔn)有很多,比如:
- 改動一個模塊或類的代碼,受影響的模塊或類是否有很多;
- 改動一個模塊或類的代碼,依賴的模塊或者類是否需要改動;
- 代碼的可測試性是否好…
直接的衡量標(biāo)準(zhǔn)是把模塊與模塊之間,及其類與類之間的依賴關(guān)系畫出來,根據(jù)依賴關(guān)系圖的復(fù)雜性來判斷是否需要解耦重構(gòu)。
如何給代碼 “解耦”?
給代碼解耦的方法有:封裝與抽象、中間層、模塊化,以及一些其他的設(shè)計思想與原則,比如:單一職責(zé)原則、基于接口而非實現(xiàn)編程、依賴注入、多用組合少用繼承、迪米特法則。當(dāng)然還有一些設(shè)計模式,比如觀察者模式。
5.小型重構(gòu):編碼規(guī)范
前面講了很多設(shè)計原則,后面還會講到設(shè)計模式,利用好它們都可以有效地改善改代碼的質(zhì)量。但是,這些知識的合理應(yīng)用非常依賴個人經(jīng)驗,有時候用不好會適得其反。但是編碼規(guī)范正好相反,大部分都簡單明了,在代碼的細節(jié)方面,能立竿見影地改善質(zhì)量。此外,前面也講到,持續(xù)低層次小規(guī)模重構(gòu)依賴的基本上都是這些編程規(guī)范,也是改善代碼可讀性的有效手段。
我總結(jié)羅列了 20 條編碼規(guī)范,分為三個大類:命名與注釋、代碼風(fēng)格、編程技巧。
命名與注釋
- 命名的關(guān)鍵是能準(zhǔn)確的達意。對于不同作用域的命名,可以適當(dāng)?shù)倪x擇不同的長度,作用域小的命名,比如臨時變量等,可以適當(dāng)?shù)倪x擇短一些的命名方式。此外,命名中也可以使用耳熟能詳?shù)目s寫。
- 借助類的信息來簡化屬性、函數(shù)的命名,利用函數(shù)的信息來簡化函數(shù)參數(shù)的命名。
- 命名要可讀、可搜索。不要使用生僻的、不好讀的英文單詞來命名。此外,命名要符合項目的統(tǒng)一規(guī)范,也不要用那些反直覺的命名。
- 接口有兩種命名方式。一種是在接口中帶前綴 “I”,另一種是在接口的實現(xiàn)類中帶后綴 “Impl”。兩種命名方式都可以,關(guān)鍵要在項目中統(tǒng)一。對于抽象類的命名,我們更傾向于帶有前綴 “Abstract”。
- 注釋的目的就是讓代碼更容易看懂,只要符合這個 要求,你就可以寫??偨Y(jié)以下的話,注釋主要包含這樣三個方面的內(nèi)容:做什么、為什么、怎么做。對于一些復(fù)雜的類和接口,我們可能還需要寫明“如何用”。
- 注釋本身有一定的維護成本,所以并非越多越好。類和函數(shù)一定要寫注釋,而且要寫的盡可能全面詳細些,而函數(shù)內(nèi)部的注釋會相對少一些,一般都是靠好的命名和提煉函數(shù)、解釋性變量、總結(jié)性注釋來做到代碼易讀。
代碼風(fēng)格
代碼風(fēng)格沒有對錯之分,不同的編程語言風(fēng)格都不太一樣,只要能在團隊、項目中統(tǒng)一即可,不過,最好能跟業(yè)務(wù)推薦的風(fēng)格、開源項目的代碼風(fēng)格一致。所以,這里就不展開羅列了,你可以對照這自己屬性的編程語言的代碼風(fēng)格,自己復(fù)習(xí)以下。
編程技巧
- 將復(fù)雜的邏輯提煉拆分成函數(shù)和類;
- 通過拆分多個函數(shù)的方式來處理參數(shù)過多的情況;
- 通過參數(shù)封裝為對象來處理參數(shù)過多的情況;
- 函數(shù)中不要使用參數(shù)來做代碼執(zhí)行邏輯的控制;
- 移除過深的嵌套層次,方法包括:去掉多余的 if 或 else 語句,使用 continue、break、return 關(guān)鍵字提前退出嵌套,調(diào)整執(zhí)行順序來減少嵌套,將部分嵌套邏輯抽象成函數(shù);
- 用字面常量取代魔法值;
- 利用解釋下變量來解釋復(fù)雜表達式。
統(tǒng)一編碼規(guī)范
除了細節(jié)的知識點外,最后,還有一條非常重要的,那就是,項目、團隊,甚至公司,一定要制定統(tǒng)一的編碼規(guī)范,并且通過 Code Review 督促執(zhí)行,這對提供代碼質(zhì)量有立桿見影的效果。