編寫高質量可維護的代碼既是程序員的基本修養,也是能決定項目成敗的關鍵因素,本文試圖總結出問題項目廣泛存在的共性問題並給出相應的解決方案。vue
1. 程序員的宿命?程序員的職業生涯中不免遇到爛項目,有些項目是你加入時已經爛了,有些是本身從頭開始親手作成了爛項目,有些是從裏到外的爛,有些是表面光鮮等你深刻進去發現是個「焦油坑」,有些是此時還沒爛可是已經出現問題徵兆走在了腐爛的路上。java
國內基本上是這樣,國外狀況我瞭解很少,不過從英文社區和技術媒體上老外同行的抱怨程度看,應該是差很少的,雖然總體素質可能更高,可是也因更久的信息化而積累了更多問題。畢竟「焦油坑、Shit_Mountain 屎山」這些舶來的術語不是平白無故被髮明出來的。程序員
Any way,這大概就是咱們這個行業的宿命——要麼改行,要麼就是與爛項目爛代碼長相伴。
就像宇宙的「熵增長定律」同樣:redis
孤立系統的一切自發過程均向着令其狀態更無序的方向發展,若是要使系統恢復到原先的有序狀態是不可能的,除非外界對它作功。算法
面對這宿命的陰影,有些人認命了麻木了,逐漸對這個行業失去熱情。spring
那些不認命的選擇與之抗爭,可是地上並無路,當年軟件危機的陰雲也從未真正散去,人月神話仍然是神話,因而人們作出了各自不一樣的判斷和嘗試:sql
若是把一個問題項目比做病入膏肓的病人,那麼這三種作法分別至關因而放棄治療、截肢手術、保守治療。docker
2. 一個 35+ 程序員的反思年輕時候我也是掀桌子派和激進派的,新工程新框架大開大合,一路走來經驗值技能樹蹭蹭的漲,跳槽加薪好不快活。數據庫
可是近幾年隨着年齡增加,一方面新東西學不動了,另外一方面對經歷過的項目反思的多了觀念逐漸改變了。編程
對我觸動最大的一件事是那個我在 2016 年初開始從零搭建起的項目,在我 2018 年末離開的時候(僅從代碼質量角度)已經讓我很不滿意了。只是,這一次沒有任何藉口了:
因而我意識到一個很是淺顯的道理:擁有一張空白的畫卷、一支最高級的畫筆、一間專業的畫室,沒法保證你能夠畫出美麗的畫卷。若是你不善於畫畫,那麼一切都是空想和意淫。
而後我變成了一個「保守改良派」,由於我意識到掀桌子和激進的改革都是不負責任的,說很差聽的那樣實際上是掩耳盜鈴、逃避困難,人不可能逃避一生,你總要面對。
即使掀了桌子另起爐竈了,你仍是須要找到一種辦法把這個新的爐竈燒好,由於隨着項目發展以前的老問題仍是會一個一個冒出來,仍是須要面對現實、不逃避、找辦法。
面對問題不只有助於你把當前項目作好,也一樣有助於未來有新的項目時更好的把握住機會。
不管是職業生涯仍是天然年齡,人到了這個階段都開始喜歡回顧和總結,也變得比過去更在意項目、產品乃至公司的商業成敗。
軟件開發做爲一種商業活動,判斷其成敗的依據應該是:可否以可接受的成本、可預期的時間節奏、穩定的質量水平、持續交付知足業務須要的功能市場須要的產品。
其實就是項目管理四要素——成本、進度、範圍、質量,傳統項目管理理論認爲這四要素彼此制約難以兼得,項目管理的藝術在於四要素的平衡取捨。
關於軟件工程和項目管理的理論和著做已經不少很成熟,這裏我從程序員的視角提出一個新的觀點——質量不可妥協:
一個項目的衰敗一如一我的健康情況的惡化,固然可能有多種多樣的緣由——好比需求失控、業務調整、人員變更流失。可是做爲咱們技術人,若是能作好本身份內的工做——編寫出可維護的代碼、減小技術債利息成本、交付一個健壯靈活的應用架構,那也絕對是功德無量的。
雖然很難估算出這究竟能挽救多少項目,可是在我十多年職業生涯中,經歷的和近距離觀察的幾十個項目,確實看到了大量的項目正是因爲代碼質量不佳致使的失敗和遺憾,同時我也發現其實失敗項目的不少問題、癥結也確確實實均可以歸因到項目代碼的混亂和質量低下,好比一個常見的項目腐爛惡性循環:代碼亂》bug 多》排查問題耗時》複用度低》加班 996》士氣低落……
所謂「千里之堤,毀於蟻穴」,代碼問題就是蟻穴。
接下來,讓咱們從項目管理聚焦到項目代碼質量這個相對小的領域來深刻剖析。編寫高質量可維護的代碼是程序員的基本修養,本文試圖在代碼層面找到一些失敗項目中廣泛存在的癥結問題,同時基於我的十幾年開發經驗總結出的一些設計模式做爲藥方分享出來。
關於代碼質量的話題其實很難經過一篇文章闡述明白,甚至須要一本書的篇幅,裏面涉及到的不少概念關注點之間存在複雜微妙關係。
推薦《設計模式之美》的第二章節《從哪些維度評判代碼質量的好壞?如何具有寫出高質量代碼的能力?》,這是我看到的關於代碼質量主題最精彩深入的論述。
4. 一個失敗項目覆盤先貼幾張代碼截圖,看一下這個重病纏身的項目的病竈和症狀:
這裏先不去分析這個類的問題,只是初步展現一下病情嚴重程度。
我相信這應該不算是特別糟糕的狀況,比這個嚴重的項目俯拾皆是,可是這也應該足夠拿來暴露問題、剖析成因了。
分層的理念早已深刻人心,尤爲是業務邏輯層的獨立,完全杜絕了以前(不分層的年代)業務邏輯與展示邏輯、持久化邏輯等混雜的問題。
可是好景不長,隨着業務的複雜和變動,在業務邏輯層的複雜性也急劇增長,成爲了新的開發效率瓶頸,
問題就出在了業務邏輯組件的劃分方式——按領域模型劃分業務邏輯組件:
前面截圖的那個問題組件 ContractService 就是一個典型案例,這樣的組件每每是熱點代碼以及整個項目的開發效率的瓶頸。
問題根源的反面其實就藏着解決方案,只是須要咱們有意識的去改變習慣、遵循新的設計風格,而不是憑直覺去設計:
經典面向對象理論告訴咱們,好的代碼結構應該是「高內聚、低耦合」的:
其實這二者就是一體兩面,作到了高內聚基本也就作到了低耦合,相反若是內聚度很低,勢必存在大量高耦合的組件。
我觀察發現,很低項目都存在低內聚、高耦合的問題。根本緣由在於不少程序員,甚至是不少經驗豐富的程序員也缺乏這方面的意識——對概念不甚清楚、對危害沒有認識、對如何避免更是無從談起。
不少人從一開始就憑直覺寫程序,有了必定經驗之後通常能認識到重複代碼的危害,對複用性有很強的認識,因而就會掉進一個陷阱——盲目追求複用,結果破壞了內聚性。
軟件架構中有兩種東西來實現複用——lib 和 framework,
當咱們說「代碼中包含的業務邏輯」的時候,咱們到底在說什麼?業界並無一個標準,你們常常講的 CRUD 增刪改查其實屬於更底層的數據訪問邏輯。
個人觀點是:所謂代碼中的業務邏輯,是指這段代碼所表現出的全部輸入輸出規則、算法和行爲,一般能夠分爲如下 5 類:
固然具體到某一個組件實例,可能不會包括上述所有 5 類業務邏輯,可是也可能每一類業務邏輯存在多個。
單這樣看你可能以爲並非特別複雜,可是現實中上述 5 類業務邏輯中的每個一般還包含着一到多個底層實現邏輯,如 CRUD 數據訪問邏輯或第三方 API 的調用。
例如輸入合法性校驗,一般須要查詢對應記錄是否存在,外部接口調用前一般須要查詢相關記錄以得到調用接口須要的參數,調用接口後還須要根據結果更新相關記錄狀態。
顯然這裏存在兩個 Level 的邏輯——High Level 的與業務需求對應且關聯緊密的邏輯、Low Level 的實現邏輯。
若是對兩個 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); } }
解決「邏輯糾纏」最關鍵是要找到一種隔離機制,把兩個 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 設計模式真的很是適合解決普遍存在的邏輯糾纏問題,並且也發現不多有程序員能主動運用這個設計模式;
一部分緣由多是意識到「邏輯糾纏」問題的人本就很少,同時熟悉這個設計模式並能自如運用的人也不算多,二者的交集天然就是少得可憐;無論是什麼緣由,結果就是這個問題普遍存在成了通病。
我看到一部分對代碼質量有追求的程序員 他們的解決辦法是經過"結構化編程"和「模塊化編程」:
下面介紹一下 Template Method 設計模式的運用,簡單概括就是:
那麼它是如何避免上面兩個方案的 4 個侷限性的:
SpringFramework 等框架型的開源項目中,其實早已大量使用 Template Method 設計模式,這本該給咱們這些應用開發程序員帶來啓發和示範,可是很惋惜業界沒有注意到和充分發揮它的價值。
NestedBusinessTemplat 模式就是對其充分和積極的應用,前面一節提到過的複用的兩種正確姿式——打造本身的 lib 和 framework,其實 NestedBusinessTemplat 就是項目自身的 framework。
不管你的編程啓蒙語言是什麼,最先學會的邏輯控制語句必定是 if else,可是不幸的是它在你開始真正的編程工做之後,會變成一個損害項目質量的壞習慣。
幾乎全部的項目都存在 if else 氾濫的問題,可是卻沒有引發足夠重視警戒,甚至被不少程序員認爲是正常現象。
首先我來解釋一下爲何 if else 這個看上去人畜無害的東西是有害的、是須要嚴格管控的:
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 條件判斷,僅僅把被比較的值重構成常量或 enum 枚舉類型並無太大改善——使用者仍然直接依賴具體的枚舉值或常量,而不是依賴一個抽象。
因而解決方案就天然浮出水面了:在 enum 枚舉類型基礎上進一步抽象封裝,獲得一個所謂的「充血」的枚舉類型,代碼說話:
enum NOTIFY_TYPE { email,sms,wechat; } //先定義一個enum——一個只定義了值不包含任何行爲的「貧血」的枚舉類型 if(type==NOTIFY_TYPE.email){ //if判斷類型 調用不一樣通知機制的實現 。。。 }else if (type=NOTIFY_TYPE.sms){ 。。。 }else{ 。。。 }
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);
以上就是我總結出的最多見也最影響代碼質量的 4 個問題及其解決方案:
接下來就是如何動手去針對這 4 個方面進行重構了,可是事情尚未那麼簡單。
上面全部的內容雖然來自實踐經驗,可是要應用到你的具體項目,還須要一個步驟——火力偵察——弄清楚你要重構的那個模塊的邏輯脈絡、算法以至實現細節,不然貿然動手,很容易遺漏關鍵細節形成風險,重構的效率更難以保證,陷入進退兩難的尷尬境地。
我 2019 年一全年經歷了 3 個代碼十分混亂的項目,最大的收穫就是摸索出了一個梳理爛代碼的最佳實踐——CODEX:
毫無疑問這是程序員最好的時代,互聯網浪潮已經席捲了世界每一個角落,各行各業正在愈來愈多的依賴 IT。過去只有軟件公司、互聯網公司和銀行業會僱傭程序員,隨着雲計算的普及、產業互聯網和互聯網+興起,已經有愈來愈多的傳統企業開始僱傭程序員搭建 IT 系統來支撐業務運營。
資本的推進 IT 需求的旺盛,使得程序員成了稀缺人才,各大招聘平臺上,程序員的崗位數量和薪資水平長期名列前茅。
可是咱們這個羣體的總體表現怎麼樣呢,捫心自問,我以爲很難使人滿意,我所經歷過的以及近距離觀察到的項目,鮮有可以稱得上成功的。這裏的成功不是商業上的成功,僅限於做爲一個軟件項目和工程是否可以以可接受的成本和質量長期穩定的交付。
商業的短時間成功與否,不少時候與項目工程的成功與否沒有必然聯繫,一個商業上很成功的項目可能在工程上作的並很差,只是經過巨量的資金資源投入換來的暫時成功而已。
歸根結底,咱們程序員羣體須要爲本身的聲譽負責,長期來看也終究會爲本身的聲譽獲益或受損。
我認爲程序員最大的聲譽、最重要的職業素養,就是經過寫出高質量的代碼作好一個個項目、產品,來幫助團隊、幫助公司、幫助組織創造價值、增長成功的機會。
但願本文分享的經驗和方法可以對此有所幫助!
你好,我是四猿外。
一家上市公司的技術總監,管理的技術團隊一百餘人。
我從一名非計算機專業的畢業生,轉行到程序員,一路打拼,一路成長。
我會經過公衆號,
把本身的成長故事寫成文章,
把枯燥的技術文章寫成故事。