在UC瀏覽器的開發過程中,筆者經常參與或主導一些重構和設計工作。那麼如何才能避免將來大規模重構,如何寫出穩健、易維護、易擴充套件、生命力長久的程式呢?現將一些注意事項、踩坑記錄寫下來,供參考。
單向依賴
單向依賴原則是我在任何團隊都會著重強調的原則,千萬不能隨便違反。防微杜漸,從良好習慣開始,這些原則性的東西需要刻到骨子裡去,不要有絲毫妥協。迴圈依賴,就更為恐怖了,你不仔細閱讀程式碼根本就察覺不了,一旦出現問題,那將是致命的。修改成本很高,不是簡單優化能解決的,基本上就是要重構了。參考ADP(The Acyclic Dependencies Principle,無環依賴原則)。
避免太抽象太寬泛的工具庫類
千萬不要建立Common/General/Utility這樣的檔案或類,因為一旦開了先例,將來會有一大堆的東西往裡面塞,直到無法承受,且根本無法控制其壞味道蔓延。
這個觀點,我也是從別的文章看來的,算是拿來主義吧。當時看到這個說法的時候,覺得深有同感,試問我們是不是經常都在走這樣的道路呢?
如若不信,可以現在就去自己的專案中搜尋檔名:Common / Util / Utils / Tools。
我所經歷過的專案,幾乎也都幹過這樣的事情吧,通常就是覺得有一些基礎工具,感覺命名比較難,很難想到一個準確優雅的名稱,然後呢,這又是大家都需要使用的公共工具,那麼就叫Common吧,這名字還不錯,不失優雅,而且還有海納百川的範兒!然後我們的專案中就出現了Common.h,Utilities.java之類的檔案。一開始還好,裡面都是一些基礎的簡單的工具函式,但是隨著時間的推移,你會發現慢慢的也就變成了大雜燴,團隊開發的時候,凡是一時難以分類的,或者覺得反正也是公共的東西,那就往這裡面放吧。
這樣演變的結果,大家肯定不難想象,也肯定都見識過這樣的案例。我們在做資訊流重構的時候就遇到一個InfoFlowUtils類,雖然吧,它的命名已經有了InfoFlow這個約束詞了,但是對於資訊流產品來說,這個約束詞也是太強大了些,幾乎所有的工具都可以往上蹭吧。所以其結果就是,裡面包含了各種工具,例如圖形的、顏色的、字串的、日期的、dispatcher操作的、MD5加密的、16進位制運算的,真是應有盡有啊。當你移植程式碼的時候,發現你的程式碼需要依賴這些的時候,那滋味,那酸爽。。。
為了避免這類現象的再次出現,唯一的辦法就是不要再建立這樣命名的庫類。就算是對Utility這個命名情有獨鍾的,那麼命名上也請嚴格遵循SRP(單一職責原則)吧,比如UrlUtil,MathUtils,ColorUtility等。也就是在檔名前加上具體的約束,參考後面的,工具需要小而美。
防微杜漸,大家共勉。
低耦合、高內聚
這裡不闡述耦合和內聚的定義。
耦合越高,複用越難,程式碼移植也就越困難。
程式碼級的低耦合,也就是說,在能完成功能的前提下,你所需要依賴的程式碼或庫越少越好,要敢於做減法。如果你真的覺得一個需求價值不大,由於邏輯怪異而對設計、對程式碼又有很嚴重的破壞,也需要敢於去跟產品PK。
低耦合不光是程式碼級別要求低耦合,在業務邏輯上也要儘量做到低耦合。例如我們的專案裡面,有換膚功能,其中內建了兩種皮膚,日間模式和夜間模式皮膚,同時技術上可以支援多種自定義皮膚。在我們的業務程式碼中,很多地方都會去檢測當前是否夜間模式,如果是的話,就去新增一個遮罩、或者做顏色變換,甚至有做影像變換的,我並不知道影像變換的效率如何,會不會影響到介面卡頓,反正凡是影像處理,潛意識都是低效率的、耗電的東西,除非這個變換剛好能使用上硬體加速,可是安卓機型那麼多,能保證都能硬體加速嗎?
那麼對於夜間模式的特殊性來說,正確的做法是什麼呢?首先需要將處理模式從邏輯上統一化、一致化。也就是說無論日間模式、夜間模式,對業務層來說應該是透明的,業務層根本不應該關心當前是什麼皮膚模式,只應該按統一的方案來處理,需要抹平不同模式之間的差異。比如一張圖片,夜間模式需要變得暗淡一點,就不應該去修改影像的畫素色彩值,完全可以換一種方式來實現。例如無論什麼模式,我們都在影像控制元件上新增一層遮罩,這個遮罩的顏色由資源管理器(ResManager)來提供,而這個顏色是配置到皮膚資源XML檔案裡面的,既方便修改調整,也無需業務層去耦合那麼多換膚邏輯,只需要在夜間模式XML檔案裡面配置一個半透明黑色,在日間模式資源裡面配置純透明色即可。
慎用繼承
抽象不是萬能的。
能不用繼承也能很好解決問題的時候,就不要用繼承,簡單說:繼承能不用就不用。
雖然在很多場合,使用繼承有它的合理性,而且繼承也的確是一個非常有用的東東,然而一旦使用不當,就會成為災難的根源,這就是我們常說要慎用繼承的原因。
關於繼承的複雜性,可能面臨的坑,請參考物件導向設計原則之:里氏替換原則(LSP)
另外,有一種說法叫少用繼承,多用組合(聚合)。
繼承具有侵入性。
假如你需要編寫一個ClassC,繼承自ClassA,那麼你的C就被A侵入了,你必須得遵循A的方式來設計介面,C跟A的關係,必須在編譯時期就確定下來,當然就不具備執行時期再進行變化的靈活性,如果需要在某種條件下,不需要使用A的那一套特性,就會比較麻煩了。而且,就算不考慮動態變化,如果哪天產品經理說我們換一套花樣,你發現新特性跟A沒有半毛錢關係,那麼你的ClassC的程式碼得全部重寫,根本沒法移植複用。
多用組合的意思,就是你不必非要從一個類繼承新類,再新增新特性。而是全新定義一個新類,將原本想繼承的類作為你的成員,然後還可以引入更多的其它類,一起來組裝你的功能。你完全可以按新的場景來設計介面,而不是受制於原先的類,從而避免被侵入,增強了將來變更的靈活性。
我見過太多由於過度抽象或濫用繼承而導致積重難返的案例,要重構只能重寫。
避免集中管理
在我的記憶中,所經歷過的專案都曾經有過集中管理,例如Message字串或整形ID的集中定義,配置項Key的集中定義,JSBridge註冊本地方法的集中註冊等等,整個專案所有業務的某一個功能範圍的一些常量,集中編寫到一個定義檔案裡面,這種做法的好處是:
- 程式碼整齊劃一
- 用常量別名定義具體的字串或數字,避免使用中的筆誤出錯
- 不容易產生重名衝突
- 方便程式碼走查與監控
但是也有缺點:
- 集中定義檔案容易膨脹
- 不易於程式碼複用
好吧,缺點好像真不如優點多,這也許就是很多專案都樂此不疲的原因吧,而且就算有人覺得有問題,想要改變的化,壓力也是巨大的,由於歷史等種種原因幾乎很難達成目標。甚至於有時候,集中管理成為了政治正確的事情,所以極少有人會去挑戰傳統。所以我也只有在全新的從零開始的九遊iOS客戶端專案中,才徹底避免了集中管理的侵入。
集中管理的案例,在真實的專案中,我們一個專案往往由十幾個乃至幾十個業務模組構成,這些模組都會使用訊息、都會使用首選項、都會使用動態配置、部分模組會使用JSBridge。那麼這些大多數模組都會使用的東西,我們往往就會將它集中定義在一個地方,例如Message.h、JSConst.x、ConfigKeyDef.java等等。這些每一類別的業務場景資源都是統一集中管理的,大家都往裡面新增自己的常量。這帶來了一個問題,當有一天,我們想將某一個業務單元做成獨立業務模組,做成SDK從整個專案中抽離出去複用,才發現異常痛苦,之前的程式碼根本不具備複用性,很難從容抽身出去,因為已經耦合嚴重。雖然複用性差並非集中管理一個原因造成,但是也是原因之一了。而且一個專案的集中管理場景根本就不只一個,上面就提到了三個,改造成本可想而知。
那麼問題怎麼解決呢?
只要我們要使用訊息機制,就必須統一定義訊息資源,否則會跟其它模組衝突導致程式異常。
其實訊息機制並非唯一的通訊方案,我個人是不太喜歡使用訊息來作為業務模組間的通訊機制的,用路由來代替訊息機制是現在比較常見的方案了,暫且不表。
用一個簡單的案例來說明訊息定義臃腫耦合的改進辦法,例如我們的換膚功能,以前也是在Message.h中新增一個ON_THEME_CHANGED訊息,然後這個換膚模組就跟其它所有模組都產生了耦合。其實換膚模組並非必須要跟這個訊息打交道,完全可以自己定義一個觀察者介面,所有的UI模組都依賴這個換膚介面,註冊皮膚變更觀察者,當換膚事件發生時,這些UI模組都可以監聽到這個事件即可。這樣一來,就解除了換膚模組跟其它業務之間由於訊息產生的耦合。
當然這樣一來,所有的UI業務模組就都得向換膚模組註冊監聽才行,而以前只需要註冊一個訊息回撥,就能接受所有的訊息了。這不是使用原方案的理由,有些事情本身就該業務層完成的事情,沒必要回避。而且,這也並非沒有辦法解決,如果覺得每個UI模組都去註冊換膚監聽麻煩的話,完全可以在業務層作一個封裝類來註冊監聽,UI業務層Controller繼承這個封裝類即可。所以慎用抽象並非不能用抽象,只要進行推敲、合理使用即可。
另外,想在此強調一下,集中管理是嚴重違背 OCP(開放封閉原則) 的,所謂開放封閉原則,就是對擴充套件開放,對修改封閉。用通俗的話來說當你設計一個模組,別人可以在你的基礎上新增新的程式碼來擴充套件你的功能,但是無需修改你的任何程式碼。做到這一點,你就是遵循開放封閉原則的。大家可以對標自己手上的專案程式碼,看有多少做到了,多少沒有做到,需要怎麼做才能做到。
去中心化
曾看過一篇微信團隊重構的分享文章,裡面提到一個觀點,核心模組容易中心化,就是一個比較重要的模組,跟大家都有交集,於是大家都往裡面新增程式碼,最後就會出現該模組的膨脹、臃腫,直至出現嚴重問題。
跟前面講的集中管理的問題整體思路大致差不多,只是場景不同,不細說。
SDK化
當初我們在一開始做瀏覽器的時候,可能覺得我們們就做一個瀏覽器專案,可能不會去做別的產品,所以就不會太在意複用性的問題,但是隨著業務的擴充套件,會發現即便不做新產品,光產品內部也是完全可能需要複用的。如果我們能將一些業務單元,做成獨立業務模組,甚至直接做成SDK,即便不是物理上的SDK,但是我們按SDK的介面標準、依賴合理化來設計這些模組,那麼將來即便是需要重構改造,也是很容易的,通常只需要在某一模組內部進行重構即可。因為SDK的介面一定是經過推敲的,擴充套件性複用性是會很強的,不會由於業務擴充套件而做很大的改造。
例如前面提到的,我們的很多業務單元本來應該是完全獨立存在,不應該有任何關聯的,那麼這些能獨立的業務單元,就應該按SDK的標準來設計,不要產生不合理的依賴。但是,我們當初的確是將他們需要通訊的訊息、需要注入的JS介面、以及其他的很多東西,都跟整個專案資源做了統一的集中管理,而那些集中管理模組就是根本無法複用的,分離改造成本都很高。因此,當哪一天專案變得積重難返,需要重構的時候,就發現重構成本異常的大。
兄弟模組之間,不要有任何耦合。假如我們一開始就將那些模組分別設計成獨立SDK,或者按照SDK的標準來要求,那麼就不難理解那些不同業務的訊息為什麼不能集中定義了。
但是,凡是不能走極端,即便是一個非常完美的東西,也可能是雙刃劍,很難有那種放之四海而皆準的準則。比如一個業務模組內部,又是否可以集中定義的,我覺得是可以的。當裡面如果有一個可以繼續拆分獨立出去的東西時,那麼最好又不要跟整個業務集中處理了。
所以,至於到底是不是絕對能不能集中管理,不能一概而論,需要看具體場景,需要看粒度是否合適,能帶來什麼好處,又可能面臨什麼問題,最終得出一個合乎當下的合理化方案,同時具備一定的前瞻性。這個過程就叫設計。
小工具零耦合
工具、介面需要小而美。
能獨立提煉出來的東西,就儘可能設計成跟外部環境無依賴的獨立模組,哪怕暫時是放在一起統一開發的,也就是一開始設計時就要將不合理依賴去除掉,俗稱解耦。而不要等將來積重難返了,才想到重構。
例如,在一些文書處理業務裡面,我們要做文字排版與渲染功能,為了儘可能高效,可能會用到CoreText,但是,當去做的時候,發現CoreTex API的使用並沒有那麼簡單,需要查資料,做實踐,幾乎就是要去預研一番,然後再在專案中寫一個使用CoreText的類CoreTextRender,然而如果你沒有低耦合的思想,為了方便,很可能在CoreTextRender中引入專案依賴,例如字型字號、文字顏色等,甚至你還可能在CoreTextRender中使用專案中另一個工具如StringUtility,而這個StringUtility本身又可能有很複雜的依賴,然後,你的CoreTextRender根本不具備可複用性,將來在其他地方想使用了,才發現無法複用。可以想想,在我們的專案中是否存在大量的這樣的現象。這本身也是違背了SRP(單一職責原則)的。從某種意義上來說,CoreTextRender就是一個文字渲染的SDK,只是我們沒有把它做成SDK的形式而已,但是從依賴關係上來說,需要按照SDK的標準來設計。
就算有時需要妥協,做不到零耦合,也要儘量做到低耦合、高內聚。
如果我們將專案中的有複用價值的東西都按照SDK的標準來設計,或者真的將他們都做成SDK的話,那麼將來需要重構的可能就會很小,即便需要,也很容易做,不會讓人已提到重構就出現恐懼的感覺。將來要將這些類SDK重新組裝成一個新的產品也會非常容易,那些類SDK的內部程式碼,幾乎不需要修改。