很多程式設計師不知道怎麼組織程式碼、怎麼提升效率、怎麼提高程式碼的可維護性、可重用性、可擴展性、靈活性,寫出來的程式碼一團糟,但這樣一團糟的程式碼居然能正常運行。
這樣的程式碼經歷,你是否也似曾相識?
身邊好多程式設計師都會有這樣的一個經歷,過個一年半載再去查看曾經寫下的程式碼時,很吃驚的在想,這麼糟糕的程式碼,真的是我以前寫的嗎?我居然能寫出這麼糟糕的程式碼!
而對於還在維護的程式碼,此時,會萌生一種去重構的想法,或是會有更好的方式實現。此時,你與程式碼的愛恨情仇已經開始了…
本片主要從六大基本原則說起,作為設計模式的引子,敘述六大基本原則和設計模式的關係,後續會一篇設計模式,詳細介紹設計模式與日常開發。
基本的規範和約束
對於基本的規範和約束,我相信每個合格的團隊都會有一套自己的玩意,一方面統一標準,增加可讀性和可維護性,另一方面也方便離職後出現bug,後來者也能更快的去定位並解決問題。
雜亂無章的程式碼實現一個大功能,對於後來者去維護,無疑會親切的問候各路祖宗。好的編碼習慣,屬於一個合格程式設計師的自我修養,於己於人,百利而無一害。
對於開發中的規格和約束,第一個要說的就是命名。
這五年多的工作,和形形色色的人合作過,記得最多的時候,我曾同時期開發和維護五個項目,業餘時間,也曾和各路英雄好漢互相合作、互相學習和共同進步,在這個過程中,最讓我覺得頭痛的就是一些命名的不規範。
不規範的樣式很多,各種奇怪的命名都有。我曾經看過這樣一串命名,其中有兩個功能,一個叫做專欄詳情,一個叫做專欄留言,命名卻是 “ZhuanLaiDetalActivity” 和 “ZhuanLaiLiuYanActivity”,看得我很懵。
這樣的命名,就問你怕不怕!對於這樣的命名都怕了,那真沒見過世面,這個至少還能看出個大概,之前看過一些漢語拼音的縮寫,像是動檢證命名為djz,這個看起來才更懵了。
對於拼音命名,這裡說一點不知道會不會被噴,遇到過一些朋友總是喜歡拼音命名,漢語拼音是中華民族推動漢文化的偉大創舉,但是在編程的時候用拼音,真的覺得好low。
即使再牛逼的技術作支撐,寫出來的程式碼也像小學生的作品,這裡沒有看不起漢語拼音的意思,只是發表下內心的一些想法。
建議:大駝峰、小駝峰或底線命名都可以,如果沒有一個統一的標準,可以參考《阿里巴巴Java 開發手冊》,對於剛入行的朋友,更應該從命名抓起,對於以後的成長有很大的幫助。
開發手冊下載地址:https://pan.baidu.com/s/1mjZxvSW。
再者要強調的就是註解。很多人也許覺得註釋越多越好,之前也在書上看到過提倡多加註釋,我覺得不然,有些時候註釋給我們增加了很多負擔和誤解。
上次review 的時候發現,一些同事copy 我的一些程式碼的時候,其實是想做另一個功能,只是想把一些程式碼拷貝過去然後大修改(我不喜歡重複造輪子,對於相似的一些功能,最好做的靈活一點,提高程式碼的可重複使用性和靈活性)。
其實可以重用的地方很少,搞不明白為啥不自己寫那麼幾行程式碼,這都不是事,讓我很懵逼的是他們把我的備註和作者也拷貝過去了,當我進入那個類別的時候,發現作者是我,去git 查看歷史提交,完全沒我啥事,而且功能描述和此類完全不相關…
對於註釋,還有一點要說的就是一些多餘的註釋,這個叫需要和命名相結合,好的命名規範,可以省略好多不必要註釋,比如login、register,再加上登錄、註冊的註釋,完全沒必要。
良好的習慣,可以為我們開發帶來很多便捷,但有些喜歡textview1、textview2 命名的,這些就算加了註釋,等下文用到的時候,看了也是一群羊駝在奔跑,上下奔騰的那種。
1.for (int i = 0; i
2. // TODO
##3.這樣的代碼,可能很多人會覺得很正常,也會有部分人會把責任歸咎於譚浩強老師,是的,譚老的書中問題確實很多,但這不是寫這種代碼的理由。
日常開發中,還有平常維護別人程式碼的同時,總會去調試 for 語句,難道不覺得這樣的程式碼很糟糕,看得有點懵嗎?就算加了註釋,還是一坨一坨的。
因為每個團隊有自己的規範和約束,大的公司,會有一套自己的規範,統一於各個團隊,不同的語言也有不同的約束,如果日後有時間,會專門寫一篇詳細的約束與規範的blog 贈送。
對於這塊,想寫的東西真的好多好多,比如case 後面的亂用,1、2、3 總是讓人費解,比如必要的常數替代變量,比如線程池取代線程,比如必要的地方使用單例,東西真的好多好多,不再比如下去了,今天就先說明兩個重點,命名和註釋。
建議:合理使用註釋,對於新手在學習期間,在陌生的程式碼和不清晰的邏輯上,盡可能多一點註釋,便於理解,對於老鳥,盡可能規範的命名。
透過命名達到註解的效果,但是對於邏輯複雜或操作狀態太多的時候,必要的註解還是很重要的,減少維護成本。
一些應該熟知的程式設計想法
一個程式設計師用在寫程式上的時間大概佔他的工作時間的10-20%,大部分的程式設計師每天大約能寫出10-12 行的能進入最終的產品的程式碼——不管他的技術水平有多高。
好的程式設計師花去 90% 的時間在思考、研究和實驗,找出最優方案。差的程式設計師花去 90% 的時間在調試問題程序、盲目的修改程序,期望某種寫法能可行。
對於一個優秀的程式設計師來說,邏輯才是最重要的,他們願意花更多的時間做思考,這樣做的同時,就是更少bug 會出現,甚至可以把bug 率降到很低。
我並不是很優秀的開發者,但這些年依然有這麼一個習慣,對於複雜或者多樣的功能,總會先理清思路,先列舉出會有哪些操作,哪些地方是bug的雷區應該多注意,我也常會和隊友提起,一圖勝千言,理清思緒再下手,事半功倍。
不管業務邏輯是否複雜,上去就是乾,發現有何不妥的再去修改,發現漏掉的再去添加,這樣導致程式碼總是一坨一坨的堆在那裡,經過多次的修改,已經面目全非,對於維護的人來說,更是苦不堪言。
在此,筆者也建議讀者朋友,不妨試一試先繪圖在動手,把一個模組繼續拆解成一個個接口,透過實現接口去實現這個模組,做到面向接口編程,這樣可維護性會提升好多… …
對於模組與模組之間的通信,不應該是類別與類別之間的關聯,而是透過抽象去實現交互,抽像不應該依賴細節,細節應該依賴抽象,這話比較繞口,說穿了,就是面向介面編程,而不是面向實現編程。
這樣做的好處就是,將來你要把這個被呼叫的類別換成一個別的實作類別時,你就不用去把呼叫過它的類別一個個改掉了,因為它們調的是接口,接口沒變,在配置裡把接口的實現類換成新的類,就全部都替換掉了。
類別之間的耦合越弱,越有利於復用,一個處在弱耦合的類別被修改,不會對有關係的類別造成波及。
建議:理清思緒再下手,事半功倍。開發過程中,不妨先定義好接口,透過實作介面去完成模組的開發,盡可能的減少 bug 率,寫出更優質的程式碼。
技術能力的提高,從程式碼上的體現主要在於 “高內聚、低耦合”,因為這些想法衍生出許多開發模式,例如現在比較流行的 MVC、MVP、MVVM 等。
版本迭代與重構
我們在做任何系統的時候,都不要指望系統一開始時需求確定,就再也不會變化,這是不現實也不科學的想法,而既然需求是一定會改變的,那麼如何在面對需求的變化時,設計軟體的可以相對容易修改,不至於說,新需求一來,就要把整個程式推倒重來。
相信很多朋友都遇過,原本一個很普通的需求,在經歷過N 次迭代和修改後,已經形成一個龐大的功能,隨著版本的不斷迭代,維護起來的成本也隨著越來越大,這樣就形成了惡性循環,重構程式碼即將登上歷史舞台。
不可否認,從維護成本來看,重構確實是一個很不錯的方案,重構的成本比原基礎維護的成本更小,也更方便以後的維護。有些公司甚至在多次版本迭代後,直接把整個專案推到重構,這樣的事情不只發生在小公司,在一些大公司,也是會發生多次。
從技術上來說,重構複雜程式碼時要做三件事:理解舊程式碼、分解舊程式碼、建立新程式碼。
而待重構的舊程式碼往往難以理解,尤其是在多次迭代且多人經手的模組;模組之間過度耦合導致牽一發而動全身,不易控制影響範圍;舊程式碼不易測試導致無法保證新程式碼的正確性,尤其是在產品文件不全的時候。
這是上次review 時候發現的一段程式碼,先不說常數使用的不規範,這是經過一次次產品迭代後的結果,但這不是藉口,造了這麼多次輪子,真不應該。做為程式碼可重用性的反面教材,此處體現的淋漓盡致,如果有更多的狀態,此處必然還是會重複多次…
建議:重構,並不是萬能的,重構後的程式碼,當再次經歷後續幾個版本修改後,程式碼又顯的雜亂無章,那一坨碼總是不斷的重演。
既然無法確定需求日後是否會修改,那我們只能透過提高程式碼品質來應對,以不變應萬變,合理設計接口,每次更改需求時多思考,對於多次使用的程式碼進行封裝提取,盡可能少的改裝既有的邏輯。
設計模式的重要性
會建築設計的是建築工程師,不會建築設計的是搬磚的。
前面已經說了很多,現在直接說一下設計模式的重要性,提到設計模式,就必須提到六大基本原則和架構設計,提到架構設計,設計模式的重要性便可想而知。
首先,六大基本原則還是有點爭議的,我之前看到的書籍中,一般都是單一職責原則、迪米特法則、里氏替換原則、開閉原則、依賴倒置原則和介面隔離原則。
但我最近在一些帖子上看到,有一種說法是沒有接口隔離原則,而是合成/ 聚合復用原則,為了不影響之前的準備,合成/ 聚合復用原則會單獨拿出來說一下。
六大基本原則,它是整個架構設計的靈魂,是架構設計的一種指導思想,而設計模式是架構設計的一種具體設計技巧,是架構設計的具體實踐。
先從架構設計說起,對於架構設計,主要體現在抽象能力,抽象能力又依賴架構者編碼的閱歷、功能的拆解和理解、邏輯的嚴密性。
做架構設計應該盡可能且更全面的考慮問題,盡可能做好程式碼的包容性,海納百川,有容乃大之勢,這是架構設計者應該具備的基本條件。
考慮的問題越周全,包容性越強,則工作難度越大,造成自己的障礙就越多。合理的將這些細節問題抽象化,並提出解決方案,抽象程度越高,解決方案越合理,這才是架構者的價值。
從具體的需求,到程式碼實現,再到具體的產品。架構設計的目的無非是系統的複用性、擴展性與穩定性,具體的東西是無法很好地體現這些特性的,只有抽象的事物才能最好的體現。
在架構設計的過程中,單一職責原則告訴我們應該更好的體現高內聚、低耦合,這個類別是用來資料請求的,就別放一些解析json 的方法,如果這個類別是用來圖片載入的,view 的註解請隔離開,做到一個類別只負責一個職責,只有一個造成變化的原因。
如果一個類別承擔的職責越多,就等於把這些職責耦合在一起,會帶來一些不必要的維護成本。從大的角度來說,MVP 和 MVC 模式都是單一職責原則的體現,model-視圖-控制相隔離,各司其職。
迪米特法則指導我們如果兩個類別不必彼此直接通信,那麼這兩個類別就不應當發生直接的相互作用。如果其中一個類別需要呼叫另一個類別的某一個方法時,可以透過第三者轉送這個呼叫。
類別之間的耦合越弱,就越有利於復用,一個處在弱耦合的類別被修改,不會對有關係的類別造成波及。主要是強調了類別之間的鬆散耦合。
對於里氏替換原則,或許很多人沒聽過這個名詞,但是在實際開發過程中卻無時無刻不在使用,其實很簡單,子類型必須能夠替換掉它們的父類型。
舉個簡單的例子,“List list = new ArrayList();”,這麼做的好處其實很簡單,比如有一天ArrayList 滿足不了需求,需要改用LinkedList,只要要把ArrayList 替換成LinkedList,而不是把全域的list 物件都改一遍,提高了可維護性。
開閉原則是物件導向原則的核心,有兩個部分組成,對擴展開放,對修改關閉。軟體需求總是會變化的,對軟體設計人員來說,必須在不需要對原有系統進行修改的情況下,實現靈活的系統擴展。
對擴展開放,就是對抽象編程,而不是具體編程,因為抽象相對穩定,透過介面或抽象類別約束擴展,對擴展進行邊界限定,不允許出現在介面或抽象類別中不存在的public 方法。
讓類別依賴固定的抽象,所以,對修改是關閉的。這是建立在繼承和多態的基礎上,可以實現對抽象類別的繼承,透過覆寫其方法來擴展方法。
依賴倒置原則指的是依賴抽象而不是依賴於具體實現,這一塊在上述已經說過,其實就是面向接口編程而不是面向實現編程,這樣做的好處就是解決耦合。
一般情況下抽象的變化機率很小,讓使用者程式依賴抽象,實現的細節也依賴抽象。即使實作細節不斷變動,只要抽像不變,客戶程式就不需要變化。這大大降低了客戶程序與實作細節的耦合度。
介面隔離原則認為,「使用多個專門的介面總比使用單一的介面好」。
一個模組應該依賴它需要的接口,需要什麼接口就提供什麼接口,把不需要的接口剔除掉,同時也應該遵循單一職責原則,這樣避免臃腫的接口帶來的污染,將沒有關係的接口合併在一起,形成臃腫的大接口,就是對接口的一種污染。
介面的粒度也不能太小,太小會導致介面額數量劇增,對開發人員不友善;介面額粒度太大,彈性降低,無法提供客製化服務,為整體專案帶來無法預估的風險,合理的設計接口,也是一門藝術。
對於這張圖,一定存在很多的爭議,因為很多設計模式都用到了多個基本原則,上圖只是對設計模式的一個比較粗糙的總結。
強調六大基本原則(含有合成/ 聚合復用原則) 在設計模式中的具體體現,同時也說明了六大基本原則和23 種設計模式是相輔相成的,六大基本原則作為設計模式的基石與模板,設計模式是六大基本原則運用的靈活體現。
合成 / 聚合復用原則這個是存在一定的爭議。目前有的書中還是保留了合成 / 聚合復用原則去掉了介面隔離原則,合成 / 聚合復用原則指的是少用繼承,多用合成關係來實現。
合成和聚合都是物件建模關聯關係的一種,聚合表示一種弱的擁有關係,整體由部分組成,部分可以脫離整體作為一個獨立的個體存在,合成則是一種強的擁有關係,體現了嚴格的部分和整體的關係,部分和整體的生命週期一致,部分不能脫離整體。
總結:六大基本原則是物件導向思想的體現,單一職責原則與介面隔離原則體現了封裝的思想,開閉原則體現了物件的封裝與多態,而里氏替換原則是對物件繼承的規範。
至於依賴倒置原則,則是多態與抽象思想的體現。在充分理解物件導向的基礎上,掌握基本的設計原則,並且能夠在專案設計中靈活運用,就能夠改善我們的程式碼品質和結構設計。
尤其能夠保證可重複使用性、可維護性、可擴充性和靈活性,這也是理解和掌握設計模式必備的知識。
補充
對於六大基本原則,這是我們開發都應該熟記於心並且靈活運用的,對於設計模式在日常開發中的運用,有一點還是要強調的,適合自己的才是最好的。
如果此時一個模組是很輕量級,僅僅為了使用設計模式而用設計模式,這無疑也會顯得不倫不類,使項目變的臃腫,同時也帶來一些不必要的維護成本(雖然維護成本很低)。
最近開始整理資料,準備寫設計模式專題,主要是MVP 爬坑與迪米特法則、framework 與開閉原則、單例與爬坑、換膚與觀察者模式、加載列表與模板方法模式、建構函式與建造者的比較、多個第三方登入與指令模式,後續也會繼續完善,敬請期待。