一個技術總監的忠告:你精通那麼多技術,爲什麼仍是作很差一個項目?

編寫高質量可維護的代碼既是程序員的基本修養,也是能決定項目成敗的關鍵因素,本文試圖總結出問題項目廣泛存在的共性問題並給出相應的解決方案。vue

1. 程序員的宿命?

程序員的職業生涯中不免遇到爛項目,有些項目是你加入時已經爛了,有些是本身從頭開始親手作成了爛項目,有些是從裏到外的爛,有些是表面光鮮等你深刻進去發現是個「焦油坑」,有些是此時還沒爛可是已經出現問題徵兆走在了腐爛的路上。java

國內基本上是這樣,國外狀況我瞭解很少,不過從英文社區和技術媒體上老外同行的抱怨程度看,應該是差很少的,雖然總體素質可能更高,可是也因更久的信息化而積累了更多問題。畢竟「焦油坑、Shit_Mountain 屎山」這些舶來的術語不是平白無故被髮明出來的。程序員

Any way,這大概就是咱們這個行業的宿命——要麼改行,要麼就是與爛項目爛代碼長相伴。
就像宇宙的「熵增長定律」同樣:redis

孤立系統的一切自發過程均向着令其狀態更無序的方向發展,若是要使系統恢復到原先的有序狀態是不可能的,除非外界對它作功。算法

面對這宿命的陰影,有些人認命了麻木了,逐漸對這個行業失去熱情。spring

那些不認命的選擇與之抗爭,可是地上並無路,當年軟件危機的陰雲也從未真正散去,人月神話仍然是神話,因而人們作出了各自不一樣的判斷和嘗試:sql

  • 掀桌子另起爐竈派:
    • 不少人把項目作爛的緣由歸咎於項目前期的基礎沒打好、需求不穩定一路打補丁、前面的架構師和程序員留下的爛攤子難以收拾。
    • 他們要麼沒有信心去收拾爛攤子,要麼以爲這是費力不討好,因而要放棄掉項目,寄但願於出現一個機會能重頭再來。
    • 可是他們對於如何避免重蹈覆轍、作出另外一個爛項目是沒有把握也沒有深刻思考的,只是盲目樂觀的認爲本身比前任更高明。
  • 激進改革派:
    • 這個派別把緣由歸結於爛項目當初沒有采用正確的編程語言、最新最強大的技術棧或工具。
    • 他們中一部分人也想着有機會另起爐竈,用時下最流行最熱門的技術棧(spring boot、springcloud、redis、nosql、docker、vue)。
    • 或者即使不另起爐竈,也認爲現有技術棧太過期沒法容忍了(其實可能並不算過期),不用微服務不用分佈式就不能接受,因而激進的引入新技術棧,魯莽的對項目作大手術。
    • 這種對剛剛流行還不成熟技術的盲目跟風、技術選型不慎重的狀況很是廣泛,今天在他們眼中落伍的技術棧,其實也不過是幾年前另外一批人趕的時髦。
    • 我不反對技術上的追新,可是一樣的,這裏的問題是:他們對於大手術的風險和反作用,對如何避免重蹈覆轍用新技術架構作出另外一個爛項目,沒有把握也沒有深刻思考的,只是盲目樂觀的認爲新技術能帶來成功。
    • 也沒人能阻止這種簡歷驅動的技術選型浮躁風氣,畢竟花的是公司的資源,用新東西顯得本身頗有追求,失敗了也不影響簡歷美化,簡歷上只會增長一段項目履歷和幾種精通技能,不會提到又作爛了一個項目,名利雙收穩賺不賠。
  • 保守改良派:
    • 還有一類人他們不肯輕易放棄這個有問題但仍在創造效益的項目,由於他們看到了項目仍然有維護的價值,也看到了另起爐竈的難度(萬事開頭難,其實項目的冷啓動存在不少外部制約因素)、大手術對業務形成影響的代價、系統遷移的難度和風險。
    • 同時他們嘗試用溫和漸進的方式逐步改善項目質量,採用一系列工程實踐(主要包括重構熱點代碼、補自動化測試、補文檔)來清理「技術債」,消除制約項目開發效率和交付質量的瓶頸。

若是把一個問題項目比做病入膏肓的病人,那麼這三種作法分別至關因而放棄治療、截肢手術、保守治療。docker

2. 一個 35+ 程序員的反思

年輕時候我也是掀桌子派和激進派的,新工程新框架大開大合,一路走來經驗值技能樹蹭蹭的漲,跳槽加薪好不快活。數據庫

可是近幾年隨着年齡增加,一方面新東西學不動了,另外一方面對經歷過的項目反思的多了觀念逐漸改變了。編程

對我觸動最大的一件事是那個我在 2016 年初開始從零搭建起的項目,在我 2018 年末離開的時候(僅從代碼質量角度)已經讓我很不滿意了。只是,這一次沒有任何藉口了:

  • 從技術選型到架構設計到代碼規範,都是我本身作的,團隊不大,也是我本身組建和一手帶出來的;
  • 最開始的半年進展很是順利,用着我最趁手的技術和工具一路狂奔,年末前替換掉了以前採購的那個垃圾產品(對的,有個前任在業務上作參照也算是個很大的有利因素);
  • 作的過程我也算是盡心盡力,用盡畢生所學——前面 13 年工做的經驗值和走過的彎路、教訓,使得公司只用其它同類公司同類項目 20% 的資源就把平臺作起來了;
  • 若是說多快好省是最高境界,那麼當時的我算是作到了多、快、省——交付的功能很是豐富且貼近業務需求、開發節奏快速、對公司開發資源很節省;
  • 可是如今看來,「好」就遠遠沒有達到了,到了項目中期,簡單優先級高的需求都已經作完了,公司業務上出現了新的挑戰——接入另外一個核心繫統以及外部平臺,真正的考驗來了。
  • 那個改造工程影響面比較大,須要對咱們的系統作大面積修改,最麻煩的是這意味着從一個簡單的單體系統變成了一個分佈式的系統,並且業務涉及資金交易,可靠性要求較高,是難上加難。
  • 因而問題開始出現了:我以前架構的優勢——簡單直接——這個時候再也不是優勢了,簡單直接的架構在業務環境、技術環境都簡單的狀況下能夠作到多快好省,可是當業務、技術環境都陡然複雜起來時,就不行了;
  • 具體的表現就是:架構和代碼層面的結構都快速的變得複雜、混亂起來了——熵急劇增長;
  • 後面的事情就一發不可收拾:代碼改起來愈來愈吃力、測試問題變多、生產環境故障和問題變多、因而消耗在排查測試問題生產問題和修復數據方面的精力急劇增長、出現惡性循環。。。
  • 到了這個境地,項目就算是作爛了!一個我從頭開始作起的沒有任何藉口的失敗!

因而我意識到一個很是淺顯的道理:擁有一張空白的畫卷、一支最高級的畫筆、一間專業的畫室,沒法保證你能夠畫出美麗的畫卷。若是你不善於畫畫,那麼一切都是空想和意淫。

而後我變成了一個「保守改良派」,由於我意識到掀桌子和激進的改革都是不負責任的,說很差聽的那樣實際上是掩耳盜鈴、逃避困難,人不可能逃避一生,你總要面對。

即使掀了桌子另起爐竈了,你仍是須要找到一種辦法把這個新的爐竈燒好,由於隨着項目發展以前的老問題仍是會一個一個冒出來,仍是須要面對現實、不逃避、找辦法。

面對問題不只有助於你把當前項目作好,也一樣有助於未來有新的項目時更好的把握住機會。

不管是職業生涯仍是天然年齡,人到了這個階段都開始喜歡回顧和總結,也變得比過去更在意項目、產品乃至公司的商業成敗。

軟件開發做爲一種商業活動,判斷其成敗的依據應該是:可否以可接受的成本、可預期的時間節奏、穩定的質量水平、持續交付知足業務須要的功能市場須要的產品。

其實就是項目管理四要素——成本、進度、範圍、質量,傳統項目管理理論認爲這四要素彼此制約難以兼得,項目管理的藝術在於四要素的平衡取捨。

關於軟件工程和項目管理的理論和著做已經不少很成熟,這裏我從程序員的視角提出一個新的觀點——質量不可妥協

  • 質量要素不是一個能夠被犧牲和妥協的要素——犧牲質量會致使其它三要素全都受損,反之同理,追求質量會讓你在其它三個方面同時受益。
  • 在保持一個質量水平的前提下,成本、進度、範圍三要素確確實實是互相制約關係——典型的好比犧牲成本(加班加點)來加快進度交付急需的功能。
  • 正如著名的「破窗效應」所啓示的那樣:任何一種不良現象的存在,都在傳遞着一種信息,這種信息會致使不良現象的無限擴展,同時必須高度警覺那些看起來是偶然的、個別的、輕微的「過錯」,若是對這種行爲漠不關心、熟視無睹、反應遲鈍或糾正不力,就會縱容更多的人「去打爛更多的窗戶玻璃」,就極有可能演變成「千里之堤,潰於蟻穴」的惡果——質量不佳的代碼之於一個項目,正如一扇破了的窗之於一幢建築、一個螞蟻巢之於一座大堤。
  • 好消息是,只要把質量提上去項目就會逐漸走上健康的軌道,其它三個方面也都會改善。管好了質量,你就很大程度上把握住了項目成敗的關鍵因素。
  • 壞消息是,項目的質量很容易失控,現實中質量不佳、越作越臃腫混亂的項目比比皆是,質量改善越作越好的案例聞所未聞,以致於人們將其視爲如同物理學中「熵增長定律」同樣的必然規律了。
  • 固然任何事情都有一個度的問題,當質量低於某個水平時纔會致使其它三要素同時受損。反之當質量高到某個水平之後,繼續追求質量不只得不到明顯收益,並且也會損害其它三要素——邊際效用遞減定律。
  • 這個度須要你爲本身去評估和測量,若是目前的質量水平還在二者之間,那麼就應該重點改進項目質量。固然,現實世界中不多看到哪一個項目質量高到了不須要重視的程度。
3. 項目走向衰敗的最多見誘因——代碼質量不佳

一個項目的衰敗一如一我的健康情況的惡化,固然可能有多種多樣的緣由——好比需求失控、業務調整、人員變更流失。可是做爲咱們技術人,若是能作好本身份內的工做——編寫出可維護的代碼、減小技術債利息成本、交付一個健壯靈活的應用架構,那也絕對是功德無量的。

雖然很難估算出這究竟能挽救多少項目,可是在我十多年職業生涯中,經歷的和近距離觀察的幾十個項目,確實看到了大量的項目正是因爲代碼質量不佳致使的失敗和遺憾,同時我也發現其實失敗項目的不少問題、癥結也確確實實均可以歸因到項目代碼的混亂和質量低下,好比一個常見的項目腐爛惡性循環:代碼亂》bug 多》排查問題耗時》複用度低》加班 996》士氣低落……

所謂「千里之堤,毀於蟻穴」,代碼問題就是蟻穴。

接下來,讓咱們從項目管理聚焦到項目代碼質量這個相對小的領域來深刻剖析。編寫高質量可維護的代碼是程序員的基本修養,本文試圖在代碼層面找到一些失敗項目中廣泛存在的癥結問題,同時基於我的十幾年開發經驗總結出的一些設計模式做爲藥方分享出來。

關於代碼質量的話題其實很難經過一篇文章闡述明白,甚至須要一本書的篇幅,裏面涉及到的不少概念關注點之間存在複雜微妙關係。

推薦《設計模式之美》的第二章節《從哪些維度評判代碼質量的好壞?如何具有寫出高質量代碼的能力?》,這是我看到的關於代碼質量主題最精彩深入的論述。

4. 一個失敗項目覆盤

先貼幾張代碼截圖,看一下這個重病纏身的項目的病竈和症狀:

  • 這是該項目中一個最核心、最複雜也是最常常要被改動的 class,代碼行數 4881;
  • 結果就是冗長的 API 列表(列表須要滾動 4 屏才能到底,公有私有 API 180 個);

  • 仍是那個 Class,頭部的 import 延綿到了 139 行,去掉第一行 package 聲明和少許空行總共 import 引入了 130 個 class!

  • 仍是那個坑爹的組件,從 156 行開始到 235 行聲明瞭 Spring 依賴注入的組件 40 個!

這裏先不去分析這個類的問題,只是初步展現一下病情嚴重程度。

我相信這應該不算是特別糟糕的狀況,比這個嚴重的項目俯拾皆是,可是這也應該足夠拿來暴露問題、剖析成因了。

4.1 癥結 1:組件粒度過大、API 氾濫

分層的理念早已深刻人心,尤爲是業務邏輯層的獨立,完全杜絕了以前(不分層的年代)業務邏輯與展示邏輯、持久化邏輯等混雜的問題。

可是好景不長,隨着業務的複雜和變動,在業務邏輯層的複雜性也急劇增長,成爲了新的開發效率瓶頸,
問題就出在了業務邏輯組件的劃分方式——按領域模型劃分業務邏輯組件:

  • 業界關於如何設計業務邏輯層 並無標準和最佳實踐,絕大多數項目(我本身經歷過的項目以及我有機會深刻了解的項目)中你們都是想固然的按照業務領域對象來設計;
  • 例如:領域實體對象有 Account、Order、Delivery、Campaign。因而業務邏輯層就設計出 AccountService、OrderService、DeliveryService、CampaignService
  • 這種作法在項目簡單是沒什麼問題,事實上項目簡單時 你隨便怎麼設計都問題不大。
  • 可是當項目變大和複雜之後,就會出現問題了:
    • 組件臃腫:Service 組件的個數跟領域實體對象個數基本至關,必然形成個別 Service 組件變得很是臃腫——API 很是多,代碼行數達到幾千行;
    • 職責模糊:業務邏輯每每跨多個領域實體,不管放在哪一個 Service 都不合適,一樣的,要找一個功能的實現邏輯也沒法肯定在哪一個 Service 中;
    • 代碼重複 or 邏輯糾纏的兩難選擇:當遇到一個業務邏輯,其中的某個環節在另外一個業務邏輯 API 中已經實現,這時若是不想忍受重複實現和代碼,就只能去調用那個 API。但這樣就形成了業務邏輯組件之間的耦合與依賴,這種耦合與依賴很快會擴散——新的 API 又會被其它業務邏輯依賴,最終造成蜘蛛網同樣的複雜依賴甚至循環依賴;
    • 複用代碼、減小重複雖然是好的,可是複雜耦合依賴的害處也很大——趕走一隻狼引來了一隻虎。兩杯毒酒給你選!

前面截圖的那個問題組件 ContractService 就是一個典型案例,這樣的組件每每是熱點代碼以及整個項目的開發效率的瓶頸。

4.2 藥方 1:倒金字塔結構——業務邏輯組件職責單1、禁止層內依賴

問題根源的反面其實就藏着解決方案,只是須要咱們有意識的去改變習慣、遵循新的設計風格,而不是憑直覺去設計:

  • 業務邏輯層應該被設計成一個個功能很是單一的小組件,所謂小是指 API 數量少、代碼行數少;
  • 因爲職責單一所以必然組件數量多,每個組件對應一個很具體的業務功能點(或者幾個相近的);
  • 複用(調用、依賴)只應該發生在相鄰的兩層之間——上層調用下層的 API 來實現對下層功能的複用;
  • 因而系統架構就天然呈現出倒立的金字塔形狀:越接近頂層的業務場景組件數量越多,越往下層的複用性高,因而組件數量越少。

4.3 癥結 2:低內聚、高耦合

經典面向對象理論告訴咱們,好的代碼結構應該是「高內聚、低耦合」的:

  • 高內聚:組件自己應該儘量的包含其所實現功能的全部重要信息和細節,以便讓維護者無需跳轉到其它多個地方去了解必要的知識。
  • 低耦合:組件之間的互相依賴和了解儘量少,以便在一個組件須要改動時其它組件不受影響。

其實這二者就是一體兩面,作到了高內聚基本也就作到了低耦合,相反若是內聚度很低,勢必存在大量高耦合的組件。

我觀察發現,很低項目都存在低內聚、高耦合的問題。根本緣由在於不少程序員,甚至是不少經驗豐富的程序員也缺乏這方面的意識——對概念不甚清楚、對危害沒有認識、對如何避免更是無從談起。

不少人從一開始就憑直覺寫程序,有了必定經驗之後通常能認識到重複代碼的危害,對複用性有很強的認識,因而就會掉進一個陷阱——盲目追求複用,結果破壞了內聚性。

  • 業界關於「複用性」的認識存在一個誤區——認爲包括業務邏輯組件在內的任何層面的組件都應該追求最大限度的可複用性
  • 複用固然是好的,但那應該有個前提條件:不增長系統複雜度的狀況下的複用,纔是好的。
  • 什麼樣的複用會增長系統複雜性、是很差的呢?前面提到的,一個業務邏輯 API 被另外一個業務邏輯 API 複用——就是很差的:
    • 損害了穩定性:由於業務邏輯自己是跟現實世界的業務掛鉤的,而業務會發生變化;當你複用一個會發生變化的 API,至關於在沙子上建高樓——地基是鬆動的;
    • 增長了複雜性:這樣的依賴還形成代碼可讀性下降——在一個本就複雜的業務邏輯代碼中,包含了對另外一個複雜業務邏輯的調用,複雜度會急劇增長,並且會不斷氾濫和傳遞;
    • 內聚性被破壞:因爲業務邏輯被打散在了多個組件的方法內,變得支離破碎,沒法在一個地方看清總體邏輯脈絡和實現步驟——內聚性被破壞,同時也意味着,這個調用鏈條上涉及的全部組件之間存在高耦合。

4.4 藥方 2:複用的兩種正確姿式——打造本身的 lib 和 framework

軟件架構中有兩種東西來實現複用——lib 和 framework,

  • lib 庫是供你(應用程序)調用的,它幫你實現特定的能力(好比日誌、數據庫驅動、json 序列化、日期計算、http 請求)。
  • framework 框架是供你擴展的,它自己就是半個應用程序,定義好了組件劃分和交互機制,你須要按照其規則擴展出特定的實現並綁定集成到其中,來完成一個應用程序。
  • lib 就是組合方式的複用,framework 則是繼承式的複用,繼承的 Java 關鍵字是 extends,因此本質上是擴展。
  • 過去有個說法:「組合優於繼承,能用組合解決的問題儘可能不要繼承」。我不一樣意這個說法,這容易誤導初學者覺得組合優於繼承,其實繼承纔是面向對象最強大的地方,固然任何東西都不能亂用。
  • 典型的繼承亂用就是爲了得到父類的某個 API 而去繼承,繼承必定是爲了擴展,而不是爲了直接得到一個能力,得到能力應該調用 lib,父類不該該去實現具體功能,那是 lib 該作的事。
  • 也不該該爲了使用 lib 而去繼承 lib 中的 Class。lib 就是用來被組合被調用的,framework 就是用來被繼承、擴展的。
  • 再展開一下:lib 既能夠是第三方的(log4j、httpclient、fastjson),也但是你本身工程的(好比你的持久層 Dao、你的 utils);
  • framework 同理,既能夠是第三方的(springmvc、jpa、springsecurity),也能夠是你項目內封裝的面向具體業務領域的(好比 report、excel 導出、paging 或任何可複用的算法、流程)。
  • 從這個意義上說,一個項目中的代碼其實只有 3 種:自定義的 lib class、自定義的 framework 相關 class、擴展第三方或自定義 framework 的組件 class。
  • 再擴展一下:相對於過去,如今咱們已經有了足夠多的第三方 lib 和 framework 來複用,來幫助項目節省大量代碼,開發工做彷佛變成了索然無味、沒技術含量的 CRUD。可是對於業務很是複雜的項目,則須要有經驗、有抽象思惟、懂設計模式的人,去設計面向業務的 framework 和麪向業務的 lib,只有這樣才能交付可維護、可擴展、可複用的軟件架構——高質量架構,幫助項目或產品取得成功。

4.5 癥結 3:抽象不夠、邏輯糾纏——High Level 業務邏輯和 Low Level 實現邏輯糾纏

當咱們說「代碼中包含的業務邏輯」的時候,咱們到底在說什麼?業界並無一個標準,你們常常講的 CRUD 增刪改查其實屬於更底層的數據訪問邏輯。

個人觀點是:所謂代碼中的業務邏輯,是指這段代碼所表現出的全部輸入輸出規則、算法和行爲,一般能夠分爲如下 5 類:

  • 輸入合法性校驗:
  • 業務規則校驗:典型的如檢查交易記錄狀態、金額、時限、權限等,一般包含數據庫或外部接口的查詢做爲參考;
  • 數據持久化行爲:數據庫、緩存、文件、日誌等任何形式的數據寫入行爲;
  • 外部接口調用行爲;
  • 輸出/返回值準備。

固然具體到某一個組件實例,可能不會包括上述所有 5 類業務邏輯,可是也可能每一類業務邏輯存在多個。

單這樣看你可能以爲並非特別複雜,可是現實中上述 5 類業務邏輯中的每個一般還包含着一到多個底層實現邏輯,如 CRUD 數據訪問邏輯或第三方 API 的調用。

例如輸入合法性校驗,一般須要查詢對應記錄是否存在,外部接口調用前一般須要查詢相關記錄以得到調用接口須要的參數,調用接口後還須要根據結果更新相關記錄狀態。

顯然這裏存在兩個 Level 的邏輯——High Level 的與業務需求對應且關聯緊密的邏輯、Low Level 的實現邏輯。

若是對兩個 Level 的邏輯不加以區分、混爲一談,代碼質量馬上就會遭到嚴重損害:

  • 可讀性變差:兩個維度的複雜性——業務複雜性和底層實現的技術複雜性——被摻雜在了一塊兒,複雜度 1+1>2 劇增,給其餘人閱讀代碼增長很大負擔;
  • 可維護性差:可維護性一般指排查和解決問題所需花費的代價高低,當兩個 level 的邏輯糾纏在一塊兒,會使排查問題變的更困難,修復問題時也更容易出錯;
  • 可擴展性無從談起:擴展性一般指爲系統增長一個特性所需花費的代價高低,代價越高擴展性越差;與排查修復問題相似,邏輯糾纏顯然也會使添加新特性變得困難、一不當心就破壞了已有功能。

下面這段代碼就是一個典型案例——High Level 的邏輯流程(參數獲取、反序列化、參數校驗、緩存寫入、數據庫持久化、更新相關交易記錄)徹底淹沒在了 Low Level 的實現邏輯(字符串比較、Json 反序列化、redis 操做、dao 操做以及先後各類瑣碎的參數準備和返回值處理)。下一節我會針對這段問題代碼給出重構方案。

@Override
public void updateFromMQ(String compress) {
    try {
        JSONObject object = JSON.parseObject(compress);
        if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){
            throw new AppException("MQ返回參數異常");
        }
        logger.info(object.getString("mobile")+"<<<<<<<<<獲取來自MQ的受權數據>>>>>>>>>"+object.getString("type"));
        Map map = new HashMap();
        map.put("type",CrawlingTaskType.get(object.getInteger("type")));
        map.put("mobile", object.getString("mobile"));
        List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);
        redisClientTemplate.set(object.getString("mobile") + "_" + object.getString("type"),CompressUtil.compress( object.getString("data")));
        redisClientTemplate.expire(object.getString("mobile") + "_" + object.getString("type"), 2*24*60*60);
        //保存成功 存入redis 保存48小時
        CrawlingTask crawlingTask = null;
        // providType:(0:新顏,1XX支付寶,2:ZZ淘寶,3:TT淘寶)
        if (CollectionUtils.isNotEmpty(list)){
            crawlingTask = list.get(0);
            crawlingTask.setJsonStr(object.getString("data"));
        }else{
            //新增
            crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), object.getString("data"),
                    object.getString("mobile"), CrawlingTaskType.get(object.getInteger("type")));
            crawlingTask.setNeedUpdate(true);
        }
        baseDAO.saveOrUpdate(crawlingTask);
        //保存芝麻分到xyz
        if ("3".equals(object.getString("type"))){
            String data = object.getString("data");
            Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
            Map param = new HashMap();
            param.put("phoneNumber", object.getString("mobile"));
            List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
            if (list1 !=null){
                for (Dperson dperson:list1){
                    dperson.setZmScore(zmf);
                    personBaseDaoI.saveOrUpdate(dperson);
                    AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);//查詢多租戶表  身份認證、淘寶認證 爲0 置爲1
                }
            }
        }
    } catch (Exception e) {
        logger.error("更新my MQ受權信息失敗", e);
        throw new AppException(e.getMessage(),e);
    }
}

4.6 藥方 3:控制邏輯分離——業務模板 Pattern of NestedBusinessTemplate

解決「邏輯糾纏」最關鍵是要找到一種隔離機制,把兩個 Level 的邏輯分開——控制邏輯分離,分離的好處不少:

  • 根據經驗,當咱們着手維護一段代碼時,必定是想先弄清楚它的總體流程、算法和行爲,而不是一上來就去研究它的細枝末節;
  • 控制邏輯分離後,只須要去看 High Level 部分就能瞭解到上述內容,閱讀代碼的負擔大幅度下降,代碼可讀性顯著加強;
  • 讀懂代碼是後續一切維護、重構工做的前提,並且一份代碼被讀的次數遠遠高於被修改的次數(高一個數量級),所以代碼對人的可讀性再怎麼強調都不爲過,可讀性加強能夠大幅度提升系統可維護性,也是重構的最主要目標。
  • 同時,根據個人經驗,High Level 業務邏輯的變動每每比 Low Level 實現邏輯變動要來的頻繁,畢竟前者跟業務直接對應。固然不一樣類型項目狀況不同,另外它們發生變動的時間點每每也不一樣;
  • 在這樣的背景下,控制邏輯分離的好處就更明顯了:每次維護、擴充系統功能只需改動一個 Levle 的代碼,另外一個 Level 不受影響或影響很小,這會大幅下降修改爲本和風險。

我在總結過去多個項目中的教訓和經驗後,總結出了一項最佳實踐或者說是設計模式——業務模板 Pattern of NestedBusinessTemplat,能夠很是簡單、有效的分離兩類邏輯,先看代碼:

public class XyzService {

abstract class AbsUpdateFromMQ {
	public final void doProcess(String jsonStr) {
		try {
				JSONObject json = doParseAndValidate(jsonStr);
				cache2Redis(json);
				saveJsonStr2CrawingTask(json);
				updateZmScore4Dperson(json);
		} catch (Exception e) {
				logger.error("更新my MQ受權信息失敗", e);
				throw new AppException(e.getMessage(), e);
		}
	}
	protected abstract void updateZmScore4Dperson(JSONObject json);
	protected abstract void saveJsonStr2CrawingTask(JSONObject json);
	protected abstract void cache2Redis(JSONObject json);
	protected abstract JSONObject doParseAndValidate(String json) throws AppException;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public void processAuthResultDataCallback(String compress) {
    new AbsUpdateFromMQ() {
@Override
protected void updateZmScore4Dperson(JSONObject json) {
                //保存芝麻分到xyz
	            if ("3".equals(json.getString("type"))){
	                String data = json.getString("data");
	                Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
	                Map param = new HashMap();
	                param.put("phoneNumber", json.getString("mobile"));
	                List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
	                if (list1 !=null){
	                    for (Dperson dperson:list1){
	                        dperson.setZmScore(zmf);
	                        personBaseDaoI.saveOrUpdate(dperson);
	                        AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);
	                    }
	                }
	            }
}
	
@Override
protected void saveJsonStr2CrawingTask(JSONObject json) {
                   Map map = new HashMap();
    	            map.put("type",CrawlingTaskType.get(json.getInteger("type")));
    	            map.put("mobile", json.getString("mobile"));
    	            List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);
    	            CrawlingTask crawlingTask = null;
    	            // providType:(0:xx,1yy支付寶,2:zz淘寶,3:tt淘寶)
    	            if (CollectionUtils.isNotEmpty(list)){
    	                crawlingTask = list.get(0);
    	                crawlingTask.setJsonStr(json.getString("data"));
    	            }else{
    	                //新增
    	                crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), json.getString("data"),
    	                		json.getString("mobile"), CrawlingTaskType.get(json.getInteger("type")));
    	                crawlingTask.setNeedUpdate(true);
    	            }
    	            baseDAO.saveOrUpdate(crawlingTask);
}

@Override
protected void cache2Redis(JSONObject json) {
                   redisClientTemplate.set(json.getString("mobile") + "_" + json.getString("type"),CompressUtil.compress( json.getString("data")));
    	            redisClientTemplate.expire(json.getString("mobile") + "_" + json.getString("type"), 2*24*60*60);
}

@Override
protected JSONObject doParseAndValidate(String json) throws AppException {
                   JSONObject object = JSON.parseObject(json);
    	            if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){
    	                throw new AppException("MQ返回參數異常");
    	            }
    	            logger.info(object.getString("mobile")+"<<<<<<<<<獲取來自MQ的受權數據>>>>>>>>>"+object.getString("type"));
                    return object;
	}
	}.doProcess(compress);
}

若是你熟悉經典的 GOF23 種設計模式,很容易發現上面的代碼示例其實就是 Template Method 設計模式的運用,沒什麼新鮮的。

沒錯,我這個方案沒有提出和創造任何新東西,我只是在實踐中偶然發現 Template Method 設計模式真的很是適合解決普遍存在的邏輯糾纏問題,並且也發現不多有程序員能主動運用這個設計模式;
一部分緣由多是意識到「邏輯糾纏」問題的人本就很少,同時熟悉這個設計模式並能自如運用的人也不算多,二者的交集天然就是少得可憐;無論是什麼緣由,結果就是這個問題普遍存在成了通病。

我看到一部分對代碼質量有追求的程序員 他們的解決辦法是經過"結構化編程"和「模塊化編程」:

  • 把 Low Level 邏輯提取成 private function,被 High Level 代碼所在的 function 直接調用;
    • 問題 1 硬鏈接不靈活:首先,這樣雖然起到了必定的隔離效果,可是兩個 level 之間是靜態的硬關聯,Low Level 沒法被簡單的替換,替換時仍是須要修改和影響到 High Level 部分;
    • 問題 2 組件內可見性形成混亂:提取出來的 private function 在當前組件內是全局可見的——對其它無關的 High Level function 也是可見的,各個模塊之間仍然存在邏輯糾纏。這在不少項目中的熱點代碼中很常見,問題也很突出:試想一個包含幾十個 API 的組件,每一個 API 的 function 存在一兩個關聯的 private function,那這個組件內部的混亂程度、維護難度是難以承受的。
  • 把 Low Level 邏輯抽取到新的組件中,供 High Level 代碼所在的組件依賴和調用;更有經驗的程序員可能會增長一層接口而且藉助 Spring 依賴注入;
    • 問題 1 API 氾濫:提取出新的組件彷佛避免了「結構化編程」的侷限性,可是帶來了新的問題——API 氾濫:由於組件之間調用只能走 public 方法,而這個 API 其實沒有太多複用機會根本不必作成 public 這種最高可見性。
    • 問題 2 同層組件依賴失控:組件和 API 氾濫後必然致使組件之間互相依賴成爲常態,慢慢變得失控之後最終變成全部組件都依賴其它大部分組件,甚至出現循環依賴;好比那個擁有 130 個 import 和 40 個 Spring 依賴組件的 ContractService。

下面介紹一下 Template Method 設計模式的運用,簡單概括就是:

  • High Level邏輯封裝在抽象父類AbsUpdateFromMQ的一個final function中,造成一個業務邏輯的模板;
  • final function保證了其中邏輯不會被子類有意或無心的篡改破壞,所以其中封裝的必定是業務邏輯中那些相對固定不變的東西。至於那些可變的部分以及暫時不肯定的部分,以abstract protected function形式預留擴展點;
  • 子類(一個匿名內部類)像「作填空題」同樣填充,模板實現Low Level邏輯——實現那些protected function擴展點;因爲擴展點在父類中是abstract的,所以編譯器會提醒子類的程序員該擴展什麼。

那麼它是如何避免上面兩個方案的 4 個侷限性的:

  • Low Level 須要修改或替換時,只需從父類擴展出一個新的子類,父類全然不知無需任何改動;
  • 不管是父類仍是子類,其中的 function 對外層的 XyzService 組件都是不可見的,即使是父類中的 public function 也不可見,由於只有持有類的實例對象才能訪問到其中的 function;
  • 不管是父類仍是子類,它們都是做爲 XyzService 的內部類存在的,不會增長新的 java 類文件更不會增長大量無心義的 API(API 只有在被項目內複用或發佈出去供外部使用纔有意義,只有惟一的調用者的 API 是沒有必要的);
  • 組件依賴失控的問題固然也就不存在了。

SpringFramework 等框架型的開源項目中,其實早已大量使用 Template Method 設計模式,這本該給咱們這些應用開發程序員帶來啓發和示範,可是很惋惜業界沒有注意到和充分發揮它的價值。

NestedBusinessTemplat 模式就是對其充分和積極的應用,前面一節提到過的複用的兩種正確姿式——打造本身的 lib 和 framework,其實 NestedBusinessTemplat 就是項目自身的 framework。

4.7 癥結 4:無處不在的 if else 牛皮癬

不管你的編程啓蒙語言是什麼,最先學會的邏輯控制語句必定是 if else,可是不幸的是它在你開始真正的編程工做之後,會變成一個損害項目質量的壞習慣。

幾乎全部的項目都存在 if else 氾濫的問題,可是卻沒有引發足夠重視警戒,甚至被不少程序員認爲是正常現象。

首先我來解釋一下爲何 if else 這個看上去人畜無害的東西是有害的、是須要嚴格管控的

  • if else if ...else 以及相似的 switch 控制語句,本質上是一種 hard coding 硬編碼行爲,若是你贊成「magic number 魔法數字」是一種錯誤的編程習慣,那麼同理,if else 也是錯誤的 hard coding 編程風格;
  • hard coding 的問題在於當需求發生改變時,須要處處去修改,很容易遺漏和出錯;
  • 以一段代碼爲例來具體分析:
if ("3".equals(object.getString("type"))){
          String data = object.getString("data");
          Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
          Map param = new HashMap();
          param.put("phoneNumber", object.getString("mobile"));
          List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
          if (list1 !=null){
              for (Dperson dperson:list1){
                  dperson.setZmScore(zmf);
                  personBaseDaoI.saveOrUpdate(dperson);
                  AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);
              }
          }
}
  • if ("3".equals(object.getString("type")))
    • 顯然這裏的"3"是一個 magic number,沒人知道 3 是什麼含義,只能推測;
    • 可是僅僅將「3」重構成常量 ABC_XYZ 並不會改善多少,由於 if (ABC_XYZ.equals(object.getString("type"))) 仍然是面向過程的編程風格,沒法擴展;
    • 處處被引用的常量 ABC_XYZ 並無比處處被 hard coding 的 magic number 好多少,只不過有了含義而已;
    • 把常量升級成 Enum 枚舉類型呢,也沒有好多少,當須要判斷的類型增長了或判斷的規則改變了,仍是須要處處修改——Shotgun Surgery(霰彈式修改)
  • 並不是全部的 if else 都有害,好比上面示例中的 if (list1 !=null) { 就是無害的,沒有必要去消除,也沒有消除它的可行性。判斷是否有害的依據:
    • 若是 if 判斷的變量狀態只有兩種可能性(好比 boolean、好比 null 判斷)時,是無傷大雅的;
    • 反之,若是 if 判斷的變量存在多種狀態,並且未來可能會增長新的狀態,那麼這就是個問題;
    • switch 判斷語句無疑是有害的,由於使用 switch 的地方每每存在不少種狀態。

4.8 藥方 4:充血枚舉類型——Rich Enum Type

正如前面分析呈現的那樣,對於代碼中普遍存在的狀態、類型 if 條件判斷,僅僅把被比較的值重構成常量或 enum 枚舉類型並無太大改善——使用者仍然直接依賴具體的枚舉值或常量,而不是依賴一個抽象。

因而解決方案就天然浮出水面了:在 enum 枚舉類型基礎上進一步抽象封裝,獲得一個所謂的「充血」的枚舉類型,代碼說話:

  • 實現多種系統通知機制,傳統作法:
enum NOTIFY_TYPE {    email,sms,wechat;  }  //先定義一個enum——一個只定義了值不包含任何行爲的「貧血」的枚舉類型

if(type==NOTIFY_TYPE.email){ //if判斷類型 調用不一樣通知機制的實現 
    。。。
}else if (type=NOTIFY_TYPE.sms){
    。。。
}else{
    。。。
}
  • 實現多種系統通知方式,充血枚舉類型——Rich Enum Type 模式:
enum NOTIFY_TYPE {    //一、定義一個包含通知實現機制的「充血」的枚舉類型
  email("郵件",NotifyMechanismInterface.byEmail()),
  sms("短信",NotifyMechanismInterface.bySms()),
  wechat("微信",NotifyMechanismInterface.byWechat());  
  
  String memo;
  NotifyMechanismInterface notifyMechanism;
  
  private NOTIFY_TYPE(String memo,NotifyMechanismInterface notifyMechanism){//二、私有構造函數,用於初始化枚舉值
      this.memo=memo;
      this.notifyMechanism=notifyMechanism;
  }
  //getters ...
}    

public interface  NotifyMechanismInterface{ //三、定義通知機制的接口或抽象父類
    public boolean doNotify(String msg);
 
    public static NotifyMechanismInterface byEmail(){//3.1 返回一個定義了郵件通知機制的策的實現——一個匿名內部類實例
        return new NotifyMechanismInterface(){
            public boolean doNotify(String msg){
                .......
            }
        };
    }
    public static NotifyMechanismInterface bySms(){//3.2 定義短信通知機制的實現策略
        return new NotifyMechanismInterface(){
            public boolean doNotify(String msg){
                .......
            }
        };
    } 
    public static NotifyMechanismInterface byWechat(){//3.3 定義微信通知機制的實現策略
        return new NotifyMechanismInterface(){
            public boolean doNotify(String msg){
                .......
            }
        };
    }
}

//四、使用場景
NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
  • 充血枚舉類型——Rich Enum Type 模式的優點:
    • 不難發現,這其實就是 enum 枚舉類型和 Strategy Pattern 策略模式的巧妙結合運用,
    • 當須要增長新的通知方式時,只需在枚舉類 NOTIFY_TYPE 增長一個值,同時在策略接口 NotifyMechanismInterface 中增長一個 by 方法返回對應的策略實現;
    • 當須要修改某個通知機制的實現細節,只需修改 NotifyMechanismInterface 中對應的策略實現;
    • 不管新增仍是修改通知機制,調用方徹底不受影響,仍然是 NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
  • 與傳統 Strategy Pattern 策略模式的比較優點:常見的策略模式也能消滅 if else 判斷,可是實現起來比較麻煩,須要開發更多的 class 和代碼量:
    • 每一個策略實現需單獨定義成一個 class;
    • 還須要一個 Context 類來作初始化——用 Map 把類型與對應的策略實現作映射;
    • 使用時從 Context 獲取具體的策略;
  • Rich Enum Type 的進一步的充血:
    • 上面的例子中的枚舉類型包含了行爲,所以已經算做充血模型了,可是還能夠爲其進一步充血;
    • 例若有些場景下,只是要對枚舉值作個簡單的計算得到某種 flag 標記,那就不必把計算邏輯抽象成 NotifyMechanismInterface 那樣的接口,殺雞用了牛刀;
    • 這是就能夠在枚舉類型中增長 static function 封裝簡單的計算邏輯;
  • 策略實現的進一步抽象:
    • 當各個策略實現(byEmail bySms byWechat)存在共性部分、重複邏輯時,能夠將其抽取成一個抽象父類;
    • 而後就像前一章節——業務模板 Pattern of NestedBusinessTemplate 那樣,在各個子類之間實現優雅的邏輯分離和複用。
5. 重構前的火力偵察:爲你的項目編制一套代碼庫目錄/索引——CODEX

以上就是我總結出的最多見也最影響代碼質量的 4 個問題及其解決方案:

  • 職責單1、小顆粒度、高內聚、低耦合的業務邏輯層組件——倒金字塔結構;
  • 打造項目自身的 lib 層和 framework——正確的複用姿式;
  • 業務模板 Pattern of NestedBusinessTemplate——控制邏輯分離;
  • 充血的枚舉類型 Rich Enum Type——消滅硬編碼風格的 if else 條件判斷;

接下來就是如何動手去針對這 4 個方面進行重構了,可是事情尚未那麼簡單。

上面全部的內容雖然來自實踐經驗,可是要應用到你的具體項目,還須要一個步驟——火力偵察——弄清楚你要重構的那個模塊的邏輯脈絡、算法以至實現細節,不然貿然動手,很容易遺漏關鍵細節形成風險,重構的效率更難以保證,陷入進退兩難的尷尬境地。

我 2019 年一全年經歷了 3 個代碼十分混亂的項目,最大的收穫就是摸索出了一個梳理爛代碼的最佳實踐——CODEX:

  • 在閱讀代碼過程當中,在關鍵位置添加結構化的註釋,形如://CODEX ProjectA 1 體檢預定流程 1 預定服務 API 入口

  • 所謂結構化註釋,就是在註釋內容中經過規範命名的編號前綴、分隔符等來體現出其所對應的項目、模塊、流程步驟等信息,相似文本編輯中的標題 一、二、3;
  • 而後設置 IDE 工具識別這種特殊的註釋,以便結構化的顯示。Eclipse 的 Tasks 顯示效果相似下圖;

  • 這個結構化視圖,本質上相對因而代碼庫的索引、目錄,不一樣於 javadoc 文檔,CODEX 具備更清晰的邏輯層次和更強的代碼查找便利性,在 Eclipse Tasks 中點擊就能跳轉到對應的代碼行;
  • 這些結構化註釋隨着代碼一塊兒提交後就實現了團隊共享;
  • 這樣的一份精確無誤、共享的、活的源代碼索引,無疑會對整個團隊的開發維護工做產生巨大助力。
  • 進一步的,若是在 CODEX 中添加 Markdown 關鍵字,甚至能夠將導出的 CODEX 簡單加工後,變成一張業務邏輯的 Sequence 序列圖,以下所示。

6. 總結陳詞——不要辜負這個程序員最好的時代

毫無疑問這是程序員最好的時代,互聯網浪潮已經席捲了世界每一個角落,各行各業正在愈來愈多的依賴 IT。過去只有軟件公司、互聯網公司和銀行業會僱傭程序員,隨着雲計算的普及、產業互聯網和互聯網+興起,已經有愈來愈多的傳統企業開始僱傭程序員搭建 IT 系統來支撐業務運營。

資本的推進 IT 需求的旺盛,使得程序員成了稀缺人才,各大招聘平臺上,程序員的崗位數量和薪資水平長期名列前茅。

可是咱們這個羣體的總體表現怎麼樣呢,捫心自問,我以爲很難使人滿意,我所經歷過的以及近距離觀察到的項目,鮮有可以稱得上成功的。這裏的成功不是商業上的成功,僅限於做爲一個軟件項目和工程是否可以以可接受的成本和質量長期穩定的交付。

商業的短時間成功與否,不少時候與項目工程的成功與否沒有必然聯繫,一個商業上很成功的項目可能在工程上作的並很差,只是經過巨量的資金資源投入換來的暫時成功而已。

歸根結底,咱們程序員羣體須要爲本身的聲譽負責,長期來看也終究會爲本身的聲譽獲益或受損。

我認爲程序員最大的聲譽、最重要的職業素養,就是經過寫出高質量的代碼作好一個個項目、產品,來幫助團隊、幫助公司、幫助組織創造價值、增長成功的機會。

但願本文分享的經驗和方法可以對此有所幫助!


你好,我是四猿外。

一家上市公司的技術總監,管理的技術團隊一百餘人。

我從一名非計算機專業的畢業生,轉行到程序員,一路打拼,一路成長。

我會經過公衆號,
把本身的成長故事寫成文章,
把枯燥的技術文章寫成故事。

相關文章
相關標籤/搜索