程序設計二三事 - 如何從點滴作起開發高質量項目

 在UC瀏覽器的開發過程當中,筆者曾參與或主導過屢次重構和設計工做。那麼如何才能避免未來大規模重構,如何寫出穩健、易維護、易擴展、生命力長久的程序呢?現將一些注意事項、踩坑記錄寫下來,供參考。java

單向依賴

 單向依賴原則是我在任何團隊都會着重強調的原則,千萬不能隨便違反。防微杜漸,從良好習慣開始,這些原則性的東西須要刻到骨子裏去,不要有絲毫妥協。循環依賴,就更爲恐怖了,你不仔細閱讀代碼根本就察覺不了,一旦出現問題,那將是致命的。修改爲本很高,不是簡單優化能解決的,基本上就是要重構了。參考ADP(The Acyclic Dependencies Principle,無環依賴原則)。瀏覽器

 我曾經在一個C++項目中遇到一個場景,A依賴B,B依賴C,C依賴D,最後D又會依賴A,問題的關鍵在於,A是在構造函數中使用B的,D會去調用A的方法。然而A還沒構造完畢,其方法是不能隨便調用的,所以出現崩潰。這就造成了先有雞仍是先有蛋的問題,排查這種問題很浪費時間。微信

避免太抽象太寬泛的工具庫類

 千萬不要創建Common/General/Utility這樣的文件或類,由於一旦開了先例,未來會有一大堆的東西往裏面塞,直到沒法承受,且根本沒法控制其壞味道蔓延。函數

 這個觀點,我也是從別的文章看來的,算是拿來主義吧。當時看到這個說法的時候,以爲深有同感,試問咱們是否是常常都在走這樣的道路呢?工具

 如若不信,能夠如今就去本身的項目中搜索文件名:Common / Util / Utils / Tools。post

 我所經歷過的項目,幾乎也都幹過這樣的事情吧,一般就是以爲有一些基礎工具,感受命名比較難,很難想到一個準確優雅的名稱,而後呢,這又是你們都須要使用的公共工具,那麼就叫Common吧,這名字還不錯,不失優雅,並且還有海納百川的範兒!而後咱們的項目中就出現了Common.h,Utilities.java之類的文件。一開始還好,裏面都是一些基礎的簡單的工具函數,可是隨着時間的推移,你會發現慢慢的也就變成了大雜燴,團隊開發的時候,凡是一時難以分類的,或者以爲反正也是公共的東西,那就往這裏面放吧。字體

 這樣演變的結果,你們確定不難想象,也確定都見識過這樣的案例。咱們在作信息流重構的時候就遇到一個InfoFlowUtils類,雖然吧,它的命名已經有了InfoFlow這個約束詞了,可是對於信息流產品來講,這個約束詞也是太強大了些,幾乎全部的工具均可以往上蹭吧。因此其結果就是,裏面包含了各類工具,例如圖形的、顏色的、字符串的、日期的、dispatcher操做的、MD5加密的、16進制運算的,真是應有盡有啊。當你移植代碼的時候,發現你的代碼須要依賴這些的時候,那滋味,那酸爽。。。優化

 爲了不這類現象的再次出現,惟一的辦法就是不要再創建這樣命名的庫類。就算是對Utility這個命名情有獨鍾的,那麼命名上也請嚴格遵循SRP單一職責原則)吧,好比UrlUtil,MathUtils,ColorUtility等。也就是在文件名前加上具體的約束,參考後面的,工具須要小而美。加密

 防微杜漸,你們共勉。   spa

低耦合、高內聚

 這裏不闡述耦合和內聚的定義。
 耦合越高,複用越難,代碼移植也就越困難。

 代碼級的低耦合,也就是說,在能完成功能的前提下,你所須要依賴的代碼或庫越少越好,要勇於作減法。若是你真的以爲一個需求價值不大,因爲邏輯怪異而對設計、對代碼又有很嚴重的破壞,也須要勇於去跟產品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根本不具有可複用性,未來在其餘地方想使用了,才發現沒法複用。從某種意義上來講,CoreTextRender就是一個文字渲染的SDK,只是咱們沒有把它作成SDK的形式而已,可是從依賴關係上來講,須要按照SDK的標準來設計。

 當有一天,你發現另外一個同窗也在使用CoreText作東西,而後你對他說,我已經作過了,你拿去用就是了,結果他把你的代碼拿過去,發現有一個編譯錯誤,是因爲你依賴了項目中一個東西,而後他又把那個東西拿過去,而後發現出現了N個編譯錯誤。而後他說:算了,我仍是本身從新寫吧,是否是很尷尬。能夠想一想,在咱們的項目中是否存在大量這樣的現象。當你費盡心思寫好一個獨立模塊後,忽然一天發現不知道誰在你的代碼裏面引入了一個項目依賴,而他添加的功能對你這個模塊來講,價值並不大。這個時候,你是否有砍人的衝動呢,這就是我前面一篇文章中提到的,修改一個模塊前,首先找做者或模塊負責人溝通討論的緣由。

 就算有時須要妥協,作不到零耦合,也要儘可能作到低耦合、高內聚。

 若是咱們將項目中的有複用價值的東西都按照SDK的標準來設計,或者真的將他們都作成SDK的話,那麼未來須要重構的可能就會很小,即使須要,也很容易作,不會讓人一提到重構就出現恐懼的感受。未來要將這些類SDK從新組裝成一個新的產品也會很是容易,那些類SDK的內部代碼,幾乎不須要修改。

原創文章,轉載請註明出處

相關文章
相關標籤/搜索