項目都做不好,還過啥程序員節(jié)?
1. 程序員的宿命?
孤立系統(tǒng)的一切自發(fā)過程均向著令其狀態(tài)更無序的方向發(fā)展,如果要使系統(tǒng)恢復到原先的有序狀態(tài)是不可能的,除非外界對它做功。
掀桌子另起爐灶派: 很多人把項目做爛的原因歸咎于項目前期的基礎沒打好、需求不穩(wěn)定一路打補丁、前面的架構師和程序員留下的爛攤子難以收拾。 他們要么沒有信心去收拾爛攤子,要么覺得這是費力不討好,于是要放棄掉項目,寄希望于出現(xiàn)一個機會能重頭再來。 但是他們對于如何避免重蹈覆轍、做出另一個爛項目是沒有把握也沒有深入思考的,只是盲目樂觀的認為自己比前任更高明。 激進改革派: 這個派別把原因歸結于爛項目當初沒有采用正確的編程語言、最新最強大的技術?;蚬ぞ摺?/section> 他們中一部分人也想著有機會另起爐灶,用上時下最流行最熱門的技術棧(spring boot、springcloud、redis、nosql、docker、vue)。 或者即便不另起爐灶,也認為現(xiàn)有技術棧太過時無法容忍了(其實可能并不算過時),不用微服務不用分布式就不能接受,于是激進的引入新技術棧,魯莽的對項目做大手術。 這種對剛剛流行還不成熟技術的盲目跟風、技術選型不慎重的情況非常普遍,今天在他們眼中落伍的技術棧,其實也不過是幾年前另一批人趕的時髦。 我不反對技術上的追新,但是同樣的,這里的問題是:他們對于大手術的風險和副作用,對如何避免重蹈覆轍用新技術架構做出另一個爛項目,沒有把握也沒有深入思考的,只是盲目樂觀的認為新技術能帶來成功。 也沒人能阻止這種簡歷驅動的技術選型浮躁風氣,畢竟花的是公司的資源,用新東西顯得自己很有追求,失敗了也不影響簡歷美化,簡歷上只會增加一段項目履歷和幾種精通技能,不會提到又做爛了一個項目,名利雙收穩(wěn)賺不賠。 保守改良派: 還有一類人他們不愿輕易放棄這個有問題但仍在創(chuàng)造效益的項目,因為他們看到了項目仍然有維護的價值,也看到了另起爐灶的難度(萬事開頭難,其實項目的冷啟動存在很多外部制約因素)、大手術對業(yè)務造成影響的代價、系統(tǒng)遷移的難度和風險。 同時他們嘗試用溫和漸進的方式逐步改善項目質量,采用一系列工程實踐(主要包括重構熱點代碼、補自動化測試、補文檔)來清理“技術債”,消除制約項目開發(fā)效率和交付質量的瓶頸。
2. 一個 35+ 程序員的反思
從技術選型到架構設計到代碼規(guī)范,都是我自己做的,團隊不大,也是我自己組建和一手帶出來的; 最開始的半年進展非常順利,用著我最趁手的技術和工具一路狂奔,年底前替換掉了之前采購的那個垃圾產品(對的,有個前任在業(yè)務上做參照也算是個很大的有利因素); 做的過程我也算是全力以赴,用盡畢生所學——前面 13 年工作的經驗值和走過的彎路、教訓,使得公司只用其它同類公司同類項目 20% 的資源就把平臺做起來了; 如果說多快好省是最高境界,那么當時的我算是做到了多、快、省——交付的功能非常豐富且貼近業(yè)務需求、開發(fā)節(jié)奏快速、對公司開發(fā)資源很節(jié)?。?/section> 但是現(xiàn)在看來,“好”就遠遠沒有達到了,到了項目中期,簡單優(yōu)先級高的需求都已經做完了,公司業(yè)務上出現(xiàn)了新的挑戰(zhàn)——接入另一個核心系統(tǒng)以及外部平臺,真正的考驗來了。 那個改造工程影響面比較大,需要對我們的系統(tǒng)做大面積修改,最麻煩的是這意味著從一個簡單的單體系統(tǒng)變成了一個分布式的系統(tǒng),而且業(yè)務涉及資金交易,可靠性要求較高,是難上加難。 于是問題開始出現(xiàn)了:我之前架構的優(yōu)點——簡單直接——這個時候不再是優(yōu)點了,簡單直接的架構在業(yè)務環(huán)境、技術環(huán)境都簡單的情況下可以做到多快好省,但是當業(yè)務、技術環(huán)境都陡然復雜起來時,就不行了; 具體的表現(xiàn)就是:架構和代碼層面的結構都快速的變得復雜、混亂起來了——熵急劇增加; 后面的事情就一發(fā)不可收拾:代碼改起來越來越吃力、測試問題變多、生產環(huán)境故障和問題變多、于是消耗在排查測試問題生產問題和修復數(shù)據(jù)方面的精力急劇增加、出現(xiàn)惡性循環(huán)。。。 到了這個境地,項目就算是做爛了!一個我從頭開始做起的沒有任何借口的失?。?/section>

質量要素不是一個可以被犧牲和妥協(xié)的要素——犧牲質量會導致其它三要素全都受損,反之同理,追求質量會讓你在其它三個方面同時受益。 在保持一個質量水平的前提下,成本、進度、范圍三要素確確實實是互相制約關系——典型的比如犧牲成本(加班加點)來加快進度交付急需的功能。 正如著名的“破窗效應”所啟示的那樣:任何一種不良現(xiàn)象的存在,都在傳遞著一種信息,這種信息會導致不良現(xiàn)象的無限擴展,同時必須高度警覺那些看起來是偶然的、個別的、輕微的“過錯”,如果對這種行為不聞不問、熟視無睹、反應遲鈍或糾正不力,就會縱容更多的人“去打爛更多的窗戶玻璃”,就極有可能演變成“千里之堤,潰于蟻穴”的惡果——質量不佳的代碼之于一個項目,正如一扇破了的窗之于一幢建筑、一個螞蟻巢之于一座大堤。 好消息是,只要把質量提上去項目就會逐漸走上健康的軌道,其它三個方面也都會改善。管好了質量,你就很大程度上把握住了項目成敗的關鍵因素。 壞消息是,項目的質量很容易失控,現(xiàn)實中質量不佳、越做越臃腫混亂的項目比比皆是,質量改善越做越好的案例聞所未聞,以至于人們將其視為如同物理學中“熵增加定律”一樣的必然規(guī)律了。 當然任何事情都有一個度的問題,當質量低于某個水平時才會導致其它三要素同時受損。反之當質量高到某個水平以后,繼續(xù)追求質量不僅得不到明顯收益,而且也會損害其它三要素——邊際效用遞減定律。 這個度需要你為自己去評估和測量,如果目前的質量水平還在兩者之間,那么就應該重點改進項目質量。當然,現(xiàn)實世界中很少看到哪個項目質量高到了不需要重視的程度。
3. 項目走向衰敗的最常見誘因——代碼質量不佳
4. 一個失敗項目復盤
這是該項目中一個最核心、最復雜也是最經常要被改動的 class,代碼行數(shù) 4881; 結果就是冗長的 API 列表(列表需要滾動 4 屏才能到底,公有私有 API 180 個);

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

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

4.1 癥結 1:組件粒度過大、API 泛濫
業(yè)界關于如何設計業(yè)務邏輯層 并沒有標準和最佳實踐,絕大多數(shù)項目(我自己經歷過的項目以及我有機會深入了解的項目)中大家都是想當然的按照業(yè)務領域對象來設計; 例如:領域實體對象有 Account、Order、Delivery、Campaign。于是業(yè)務邏輯層就設計出 AccountService、OrderService、DeliveryService、CampaignService 這種做法在項目簡單是沒什么問題,事實上項目簡單時 你隨便怎么設計都問題不大。 但是當項目變大和復雜以后,就會出現(xiàn)問題了: 組件臃腫:Service 組件的個數(shù)跟領域實體對象個數(shù)基本相當,必然造成個別 Service 組件變得非常臃腫——API 非常多,代碼行數(shù)達到幾千行; 職責模糊:業(yè)務邏輯往往跨多個領域實體,無論放在哪個 Service 都不合適,同樣的,要找一個功能的實現(xiàn)邏輯也無法確定在哪個 Service 中; 代碼重復 or 邏輯糾纏的兩難選擇:當遇到一個業(yè)務邏輯,其中的某個環(huán)節(jié)在另一個業(yè)務邏輯 API 中已經實現(xiàn),這時如果不想忍受重復實現(xiàn)和代碼,就只能去調用那個 API。但這樣就造成了業(yè)務邏輯組件之間的耦合與依賴,這種耦合與依賴很快會擴散——新的 API 又會被其它業(yè)務邏輯依賴,最終形成蜘蛛網一樣的復雜依賴甚至循環(huán)依賴; 復用代碼、減少重復雖然是好的,但是復雜耦合依賴的害處也很大——趕走一只狼引來了一只虎。兩杯毒酒給你選!
4.2 藥方 1:倒金字塔結構——業(yè)務邏輯組件職責單一、禁止層內依賴
業(yè)務邏輯層應該被設計成一個個功能非常單一的小組件,所謂小是指 API 數(shù)量少、代碼行數(shù)少; 由于職責單一因此必然組件數(shù)量多,每一個組件對應一個很具體的業(yè)務功能點(或者幾個相近的); 復用(調用、依賴)只應該發(fā)生在相鄰的兩層之間——上層調用下層的 API 來實現(xiàn)對下層功能的復用; 于是系統(tǒng)架構就自然呈現(xiàn)出倒立的金字塔形狀:越接近頂層的業(yè)務場景組件數(shù)量越多,越往下層的復用性高,于是組件數(shù)量越少。
4.3 癥結 2:低內聚、高耦合
高內聚:組件本身應該盡可能的包含其所實現(xiàn)功能的所有重要信息和細節(jié),以便讓維護者無需跳轉到其它多個地方去了解必要的知識。 低耦合:組件之間的互相依賴和了解盡可能少,以便在一個組件需要改動時其它組件不受影響。
業(yè)界關于“復用性”的認識存在一個誤區(qū)——認為包括業(yè)務邏輯組件在內的任何層面的組件都應該追求最大限度的可復用性; 復用當然是好的,但那應該有個前提條件:不增加系統(tǒng)復雜度的情況下的復用,才是好的。 什么樣的復用會增加系統(tǒng)復雜性、是不好的呢?前面提到的,一個業(yè)務邏輯 API 被另一個業(yè)務邏輯 API 復用——就是不好的: 損害了穩(wěn)定性:因為業(yè)務邏輯本身是跟現(xiàn)實世界的業(yè)務掛鉤的,而業(yè)務會發(fā)生變化;當你復用一個會發(fā)生變化的 API,相當于在沙子上建高樓——地基是松動的; 增加了復雜性:這樣的依賴還造成代碼可讀性降低——在一個本就復雜的業(yè)務邏輯代碼中,包含了對另一個復雜業(yè)務邏輯的調用,復雜度會急劇增加,而且會不斷泛濫和傳遞; 內聚性被破壞:由于業(yè)務邏輯被打散在了多個組件的方法內,變得支離破碎,無法在一個地方看清整體邏輯脈絡和實現(xiàn)步驟——內聚性被破壞,同時也意味著,這個調用鏈條上涉及的所有組件之間存在高耦合。
4.4 藥方 2:復用的兩種正確姿勢——打造自己的 lib 和 framework
lib 庫是供你(應用程序)調用的,它幫你實現(xiàn)特定的能力(比如日志、數(shù)據(jù)庫驅動、json 序列化、日期計算、http 請求)。 framework 框架是供你擴展的,它本身就是半個應用程序,定義好了組件劃分和交互機制,你需要按照其規(guī)則擴展出特定的實現(xiàn)并綁定集成到其中,來完成一個應用程序。 lib 就是組合方式的復用,framework 則是繼承式的復用,繼承的 Java 關鍵字是 extends,所以本質上是擴展。 過去有個說法:“組合優(yōu)于繼承,能用組合解決的問題盡量不要繼承”。我不同意這個說法,這容易誤導初學者以為組合優(yōu)于繼承,其實繼承才是面向對象最強大的地方,當然任何東西都不能亂用。 典型的繼承亂用就是為了獲得父類的某個 API 而去繼承,繼承一定是為了擴展,而不是為了直接獲得一個能力,獲得能力應該調用 lib,父類不應該去實現(xiàn)具體功能,那是 lib 該做的事。 也不應該為了使用 lib 而去繼承 lib 中的 Class。lib 就是用來被組合被調用的,framework 就是用來被繼承、擴展的。 再展開一下:lib 既可以是第三方的(log4j、httpclient、fastjson),也可是你自己工程的(比如你的持久層 Dao、你的 utils); framework 同理,既可以是第三方的(springmvc、jpa、springsecurity),也可以是你項目內封裝的面向具體業(yè)務領域的(比如 report、excel 導出、paging 或任何可復用的算法、流程)。 從這個意義上說,一個項目中的代碼其實只有 3 種:自定義的 lib class、自定義的 framework 相關 class、擴展第三方或自定義 framework 的組件 class。 再擴展一下:相對于過去,現(xiàn)在我們已經有了足夠多的第三方 lib 和 framework 來復用,來幫助項目節(jié)省大量代碼,開發(fā)工作似乎變成了索然無味、沒技術含量的 CRUD。但是對于業(yè)務非常復雜的項目,則需要有經驗、有抽象思維、懂設計模式的人,去設計面向業(yè)務的 framework 和面向業(yè)務的 lib,只有這樣才能交付可維護、可擴展、可復用的軟件架構——高質量架構,幫助項目或產品取得成功。
4.5 癥結 3:抽象不夠、邏輯糾纏——High Level 業(yè)務邏輯和 Low Level 實現(xiàn)邏輯糾纏
輸入合法性校驗; 業(yè)務規(guī)則校驗:典型的如檢查交易記錄狀態(tài)、金額、時限、權限等,通常包含數(shù)據(jù)庫或外部接口的查詢作為參考; 數(shù)據(jù)持久化行為:數(shù)據(jù)庫、緩存、文件、日志等任何形式的數(shù)據(jù)寫入行為; 外部接口調用行為; 輸出/返回值準備。
可讀性變差:兩個維度的復雜性——業(yè)務復雜性和底層實現(xiàn)的技術復雜性——被摻雜在了一起,復雜度 1+1>2 劇增,給其他人閱讀代碼增加很大負擔; 可維護性差:可維護性通常指排查和解決問題所需花費的代價高低,當兩個 level 的邏輯糾纏在一起,會使排查問題變的更困難,修復問題時也更容易出錯; 可擴展性無從談起:擴展性通常指為系統(tǒng)增加一個特性所需花費的代價高低,代價越高擴展性越差;與排查修復問題類似,邏輯糾纏顯然也會使添加新特性變得困難、一不小心就破壞了已有功能。
@Overridepublic 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返回參數(shù)異常");}logger.info(object.getString("mobile")+"<<<<<<<<<獲取來自MQ的授權數(shù)據(jù)>>>>>>>>>"+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);//保存芝麻分到xyzif ("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:控制邏輯分離——業(yè)務模板 Pattern of NestedBusinessTemplate
根據(jù)經驗,當我們著手維護一段代碼時,一定是想先弄清楚它的整體流程、算法和行為,而不是一上來就去研究它的細枝末節(jié); 控制邏輯分離后,只需要去看 High Level 部分就能了解到上述內容,閱讀代碼的負擔大幅度降低,代碼可讀性顯著增強; 讀懂代碼是后續(xù)一切維護、重構工作的前提,而且一份代碼被讀的次數(shù)遠遠高于被修改的次數(shù)(高一個數(shù)量級),因此代碼對人的可讀性再怎么強調都不為過,可讀性增強可以大幅度提高系統(tǒng)可維護性,也是重構的最主要目標。 同時,根據(jù)我的經驗,High Level 業(yè)務邏輯的變更往往比 Low Level 實現(xiàn)邏輯變更要來的頻繁,畢竟前者跟業(yè)務直接對應。當然不同類型項目情況不一樣,另外它們發(fā)生變更的時間點往往也不同; 在這樣的背景下,控制邏輯分離的好處就更明顯了:每次維護、擴充系統(tǒng)功能只需改動一個 Levle 的代碼,另一個 Level 不受影響或影響很小,這會大幅降低修改成本和風險。
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() {@Overrideprotected void updateZmScore4Dperson(JSONObject json) {//保存芝麻分到xyzif ("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);}}}}@Overrideprotected 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);}@Overrideprotected 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);}@Overrideprotected 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返回參數(shù)異常");}logger.info(object.getString("mobile")+"<<<<<<<<<獲取來自MQ的授權數(shù)據(jù)>>>>>>>>>"+object.getString("type"));return object;}}.doProcess(compress);}
把 Low Level 邏輯提取成 private function,被 High Level 代碼所在的 function 直接調用; 問題 1 硬連接不靈活:首先,這樣雖然起到了一定的隔離效果,但是兩個 level 之間是靜態(tài)的硬關聯(lián),Low Level 無法被簡單的替換,替換時還是需要修改和影響到 High Level 部分; 問題 2 組件內可見性造成混亂:提取出來的 private function 在當前組件內是全局可見的——對其它無關的 High Level function 也是可見的,各個模塊之間仍然存在邏輯糾纏。這在很多項目中的熱點代碼中很常見,問題也很突出:試想一個包含幾十個 API 的組件,每個 API 的 function 存在一兩個關聯(lián)的 private function,那這個組件內部的混亂程度、維護難度是難以承受的。 把 Low Level 邏輯抽取到新的組件中,供 High Level 代碼所在的組件依賴和調用;更有經驗的程序員可能會增加一層接口并且借助 Spring 依賴注入; 問題 1 API 泛濫:提取出新的組件似乎避免了“結構化編程”的局限性,但是帶來了新的問題——API 泛濫:因為組件之間調用只能走 public 方法,而這個 API 其實沒有太多復用機會根本沒必要做成 public 這種最高可見性。 問題 2 同層組件依賴失控:組件和 API 泛濫后必然導致組件之間互相依賴成為常態(tài),慢慢變得失控以后最終變成所有組件都依賴其它大部分組件,甚至出現(xiàn)循環(huán)依賴;比如那個擁有 130 個 import 和 40 個 Spring 依賴組件的 ContractService。
High Level邏輯封裝在抽象父類AbsUpdateFromMQ的一個final function中,形成一個業(yè)務邏輯的模板; final function保證了其中邏輯不會被子類有意或無意的篡改破壞,因此其中封裝的一定是業(yè)務邏輯中那些相對固定不變的東西。至于那些可變的部分以及暫時不確定的部分,以abstract protected function形式預留擴展點; 子類(一個匿名內部類)像“做填空題”一樣,填充模板實現(xiàn)Low Level邏輯——實現(xiàn)那些protected function擴展點;由于擴展點在父類中是abstract的,因此編譯器會提醒子類的程序員該擴展什么。
Low Level 需要修改或替換時,只需從父類擴展出一個新的子類,父類全然不知無需任何改動; 無論是父類還是子類,其中的 function 對外層的 XyzService 組件都是不可見的,即便是父類中的 public function 也不可見,因為只有持有類的實例對象才能訪問到其中的 function; 無論是父類還是子類,它們都是作為 XyzService 的內部類存在的,不會增加新的 java 類文件更不會增加大量無意義的 API(API 只有在被項目內復用或發(fā)布出去供外部使用才有意義,只有唯一的調用者的 API 是沒有必要的); 組件依賴失控的問題當然也就不存在了。
4.7 癥結 4:無處不在的 if else 牛皮癬

if else if ...else 以及類似的 switch 控制語句,本質上是一種 hard coding 硬編碼行為,如果你同意“magic number 魔法數(shù)字”是一種錯誤的編程習慣,那么同理,if else 也是錯誤的 hard coding 編程風格; hard coding 的問題在于當需求發(fā)生改變時,需要到處去修改,很容易遺漏和出錯; 以一段代碼為例來具體分析:
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 枚舉類型呢,也沒有好多少,當需要判斷的類型增加了或判斷的規(guī)則改變了,還是需要到處修改——Shotgun Surgery(霰彈式修改) 并非所有的 if else 都有害,比如上面示例中的 if (list1 !=null) { 就是無害的,沒有必要去消除,也沒有消除它的可行性。判斷是否有害的依據(jù): 如果 if 判斷的變量狀態(tài)只有兩種可能性(比如 boolean、比如 null 判斷)時,是無傷大雅的; 反之,如果 if 判斷的變量存在多種狀態(tài),而且將來可能會增加新的狀態(tài),那么這就是個問題; switch 判斷語句無疑是有害的,因為使用 switch 的地方往往存在很多種狀態(tài)。
4.8 藥方 4:充血枚舉類型——Rich Enum Type
實現(xiàn)多種系統(tǒng)通知機制,傳統(tǒng)做法:
enum NOTIFY_TYPE { email,sms,wechat; } //先定義一個enum——一個只定義了值不包含任何行為的“貧血”的枚舉類型if(type==NOTIFY_TYPE.email){ //if判斷類型 調用不同通知機制的實現(xiàn)。。。}else if (type=NOTIFY_TYPE.sms){。。。}else{。。。}
實現(xiàn)多種系統(tǒng)通知方式,充血枚舉類型——Rich Enum Type 模式:
enum NOTIFY_TYPE { //1、定義一個包含通知實現(xiàn)機制的“充血”的枚舉類型email("郵件",NotifyMechanismInterface.byEmail()),sms("短信",NotifyMechanismInterface.bySms()),wechat("微信",NotifyMechanismInterface.byWechat());String memo;NotifyMechanismInterface notifyMechanism;private NOTIFY_TYPE(String memo,NotifyMechanismInterface notifyMechanism){//2、私有構造函數(shù),用于初始化枚舉值this.memo=memo;this.notifyMechanism=notifyMechanism;}//getters ...}public interface NotifyMechanismInterface{ //3、定義通知機制的接口或抽象父類public boolean doNotify(String msg);public static NotifyMechanismInterface byEmail(){//3.1 返回一個定義了郵件通知機制的策的實現(xiàn)——一個匿名內部類實例return new NotifyMechanismInterface(){public boolean doNotify(String msg){.......}};}public static NotifyMechanismInterface bySms(){//3.2 定義短信通知機制的實現(xiàn)策略return new NotifyMechanismInterface(){public boolean doNotify(String msg){.......}};}public static NotifyMechanismInterface byWechat(){//3.3 定義微信通知機制的實現(xiàn)策略return new NotifyMechanismInterface(){public boolean doNotify(String msg){.......}};}}//4、使用場景NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
充血枚舉類型——Rich Enum Type 模式的優(yōu)勢: 不難發(fā)現(xiàn),這其實就是 enum 枚舉類型和 Strategy Pattern 策略模式的巧妙結合運用; 當需要增加新的通知方式時,只需在枚舉類 NOTIFY_TYPE 增加一個值,同時在策略接口 NotifyMechanismInterface 中增加一個 by 方法返回對應的策略實現(xiàn); 當需要修改某個通知機制的實現(xiàn)細節(jié),只需修改 NotifyMechanismInterface 中對應的策略實現(xiàn); 無論新增還是修改通知機制,調用方完全不受影響,仍然是 NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg); 與傳統(tǒng) Strategy Pattern 策略模式的比較優(yōu)勢:常見的策略模式也能消滅 if else 判斷,但是實現(xiàn)起來比較麻煩,需要開發(fā)更多的 class 和代碼量: 每個策略實現(xiàn)需單獨定義成一個 class; 還需要一個 Context 類來做初始化——用 Map 把類型與對應的策略實現(xiàn)做映射; 使用時從 Context 獲取具體的策略; Rich Enum Type 的進一步的充血: 上面的例子中的枚舉類型包含了行為,因此已經算作充血模型了,但是還可以為其進一步充血; 例如有些場景下,只是要對枚舉值做個簡單的計算獲得某種 flag 標記,那就沒必要把計算邏輯抽象成 NotifyMechanismInterface 那樣的接口,殺雞用了牛刀; 這時就可以在枚舉類型中增加 static function 封裝簡單的計算邏輯; 策略實現(xiàn)的進一步抽象: 當各個策略實現(xiàn)(byEmail bySms byWechat)存在共性部分、重復邏輯時,可以將其抽取成一個抽象父類; 然后就像前一章節(jié)——業(yè)務模板 Pattern of NestedBusinessTemplate 那樣,在各個子類之間實現(xiàn)優(yōu)雅的邏輯分離和復用。
5. 重構前的火力偵察:為你的項目編制一套代碼庫目錄/索引——CODEX
職責單一、小顆粒度、高內聚、低耦合的業(yè)務邏輯層組件——倒金字塔結構; 打造項目自身的 lib 層和 framework——正確的復用姿勢; 業(yè)務模板 Pattern of NestedBusinessTemplate——控制邏輯分離; 充血的枚舉類型 Rich Enum Type——消滅硬編碼風格的 if else 條件判斷;
在閱讀代碼過程中,在關鍵位置添加結構化的注釋,形如://CODEX ProjectA 1 體檢預約流程 1 預約服務 API 入口


所謂結構化注釋,就是在注釋內容中通過規(guī)范命名的編號前綴、分隔符等來體現(xiàn)出其所對應的項目、模塊、流程步驟等信息,類似文本編輯中的標題 1、2、3; 然后設置 IDE 工具識別這種特殊的注釋,以便結構化的顯示。Eclipse 的 Tasks 顯示效果類似下圖;

這個結構化視圖,本質上相對于是代碼庫的索引、目錄,不同于 javadoc 文檔,CODEX 具有更清晰的邏輯層次和更強的代碼查找便利性,在 Eclipse Tasks 中點擊就能跳轉到對應的代碼行; 這些結構化注釋隨著代碼一起提交后就實現(xiàn)了團隊共享; 這樣的一份精確無誤、共享的、活的源代碼索引,無疑會對整個團隊的開發(fā)維護工作產生巨大助力; 進一步的,如果在 CODEX 中添加 Markdown 關鍵字,甚至可以將導出的 CODEX 簡單加工后,變成一張業(yè)務邏輯的 Sequence 序列圖,如下所示。


6. 總結陳詞——不要辜負這個程序員最好的時代
評論
圖片
表情
