作者:聶曉龍(率鴿)
讀 A Philosophy of Software Design 有感,軟件設計與架構複雜度,你是戰術龍卷風嗎?
前言 有一天,一個醫生和一個土木工程師在一起争論“誰是世界上最古老的職業”。醫生說:“上帝用亞當的肋骨造出了夏娃,這是曆史上第一次外科手術,所以最古老的職業應該是醫生”,土木工程師說:“在創世紀之前,上帝從混沌中創造了天堂與人間,這是更早之前的一次土木作業,所以最古老的職業應該是土木工程”。這時軟件工程師拖着鍵盤走出來說,“那你認為,是誰創造了那片混沌?”
建築師不會輕易給100層的高樓增加一個地下室,但我們卻經常在幹這樣的事,并且總有人會對你說,“這個需求很簡單”。到土裡埋個地雷,這确實不複雜,但我們往往面臨的真實場景其實是:“在這片雷區裡加一個雷”,而雷區裡哪裡有雷,任何人都不知道 。
什麼是複雜性 我們一直在說系統很複雜,那到底什麼是複雜性?關于複雜的定義有很多種,其中比較有代表的是Thomas J. McCabe 在1976提出的理性派的複雜性度量,與John Ousterhout 教授提出的感性派的複雜性認知。
理性度量
複雜性并不是什麼新概念,早在上世紀70年代,軟件就已經極其複雜,開發與維護的成本都非常高。1976年McCabe&Associates公司開始對軟件進行結構測試,并提出了McCabe Cyclomatic Complexity Metric,我們也稱之為McCabe圈複雜度。它通過多個維度來度量軟件的複雜度,從而判斷軟件當前的開發/維護成本。
感性認知
複雜度高的代碼一定不是好代碼,但複雜度低的也不一定就是好代碼。John Ousterhout教授認為軟件的複雜性相對理性的分析,可能更偏感性的認知。
Complexity is anything that makes software hard to understand or to modify譯:所謂複雜性,就是任何使得軟件難于理解和修改的因素。
- John Ousterhout 《A Philosophy of Software Design》
50年後的今天,John Ousterhout教授在 A Philosophy of Software Design 書中提到了一個非常主觀的見解,複雜性就是任何使得軟件難于理解和修改的因素。
模糊性與依賴性是引起複雜性的2個主要因素,模糊性産生了最直接的複雜度,讓我們很難讀懂代碼真正想表達的含義,無法讀懂這些代碼,也就意味着我們更難去改變它。而依賴性又導緻了複雜性不斷傳遞,不斷外溢的複雜性最終導緻系統的無限腐化,一旦代碼變成意大利面條,幾乎不可能修複,成本将成指數倍增長。
複雜性的表現形式 複雜的系統往往也有一些非常明顯的特征,John教授将它抽象為變更放大(Change amplification)、認知負荷(Cognitive load)與未知的未知(Unknown unknowns)這3類。當我們的系統出現這3個特征,說明我們的系統已經開始逐漸變得複雜了。
症狀1-變更放大
Change amplification: a seemingly simple change requires code modifications in many different places.
譯:看似簡單的變更需要在許多不同地方進行代碼修改。
- John Ousterhout 《A Philosophy of Software Design》
變更放大(Change amplification)指得是看似簡單的變更需要在許多不同地方進行代碼修改。比較典型的代表是Ctrl-CV式代碼開發,領域模型缺少内聚與收攏,當需要對某段業務進行調整時,需要改動多個模塊以适應業務的發展。
/** * 銷售撿入客戶 */ public void pick(String salesId, String customerId) { // 查詢客戶總數 long customerCnt = customerDao.findCustomerCount(salesId); // 查詢銷售庫容 long capacity = capacityDao.findSalesCapacity(salesId); // 判斷是否超額 if(customerCnt = capacity) { throws new BizException(capacity over limit } // 代碼省略 do customer pick }
在CRM領域,銷售撿入客戶時需要進行庫容判斷,這段代碼也确實可以滿足需求。但随着業務的發展,簽約的客戶要調整為不占庫容。而客戶除了銷售撿入,還包括主管分發、leads分發、手工錄入、數據采買等多個場景,如果沒對庫容域做模型的收攏,一個簡單的邏輯調整,就需要我們在多個場景做适配才能滿足訴求。
症狀2-認知負荷
Cognitive load: how much a developer needs to know in order to complete a task.譯:開發人員需要多少知識才能完成一項任務。
- John Ousterhout 《A Philosophy of Software Design》
認知負荷(Cognitive load)是指開發人員需要多少知識才能完成一項任務。使用功能性框架時,我們希望它操作簡單,部署複雜系統時,我們希望它架構清晰,其實都是降低一項任務所需的成本。盲目的追求高端技術,設計複雜系統,增加學習與理解成本都屬于本末倒置的一種。
TMF是整個星環的支柱,也是業務中台面向可複用可擴展架構的核心。但TMF太過複雜,認知與學習成本非常高,我們日常中所面臨的一些擴展訴求99%(或者應該說100%)都不适合TMF,可能通過一些設計模式或者就是一些if else,可能更适合解決我們的問題。
除此之外,還包括一些簡單搜索場景卻用到了blink等流式引擎,簡單後台系統通過DDD進行構建,幾個商品發布的狀态機轉換用上了規則引擎等等,都屬于認知負荷複雜度的一種。
症狀3-未知的未知
Unknown unknowns: it is not obvious which pieces of code must be modified to complete a task譯:必須修改哪些代碼才能完成任務。
- John Ousterhout 《A Philosophy of Software Design》
未知的未知(Unknown unknowns)是指必須修改哪些代碼才能完成任務,或者說開發人員必須獲得哪些信息才能成功地執行任務。這一項也是John Ousterhout教授認為複雜性中最糟糕的一個表現形式。
當你維護一個有20年曆史的項目時,這種問題的出來相對而言就沒那麼意外。由于代碼的混亂與文檔的缺失,導緻你無法掌控一個500萬行代碼的應用,并且代碼本身也沒有明顯表現出它們應該要闡述的内容。這時“未知的未知”出現了,你不知道改動的這行代碼是否能讓程序正常運轉,也不知道這行代碼的改動是否又會引發新的問題。這時候我們發現,那些“上帝類”真的就隻有上帝能拯救了。
為什麼會産生複雜性 那軟件為什麼越來越複雜,是不是減少一些犯錯就能避免一場浩劫呢?回顧那些複雜的系統,我們可以找到很多因素導緻系統腐化。
想簡單圖省事,沒有及時治理不合理的内容缺少匠心追求,對肮髒代碼視而不見技術能力不夠,無法應對複雜系統交接過渡缺失,三無産品幾乎無法維護 除了上述内容外,還可以想到很多理由。但我們發現他們好像有一個共同的指向點 - 軟件工程師,似乎所有複雜的源頭就是軟件工程師的不合格導緻,所以其實一些罪惡的根因是我們自己?
1、統一的中國與分裂的歐洲
歐洲大陸面積大體與中國相當,但為什麼歐洲是分裂的,而中國是統一的。有人說他們文化不一樣,也有人說他們語言不通是主要原因,也有人說他們缺一個秦始皇。其實我們回顧歐洲的曆史,歐洲還真不缺一個大一統的帝國。羅馬帝國曾經讓地中海成為自己的内海,拿破侖鼎盛時期掌管着1300萬平方公裡的領地。歐洲也曾出現過偉大的帝國,但都未走向統一。
我們再觀察地圖,其實除了中國、俄羅斯以外,全世界99%的國家都是小國。分裂才是常态,統一才不正常。馬老師也曾說過,成功都有偶然性隻有失敗才存在必然。隻有極少國家才實現了大一統,所以我們不應該問為什麼歐洲是分裂的,而應該問為什麼中國是統一的。類比到我們的軟件也同樣如此,複雜才是常态,不複雜才不正常。
2、軟件固有的複雜性
The Complexity of software is an essential property, not an accidental one.
譯:軟件的複雜性是一個基本特征,而不是偶然如此。
- Grady Booch 《Object-Oriented Analysis and Design with Applications》
Grady Booch在 Object-Oriented Analysis and Design with Applications 中提出這樣一個觀念,他認為軟件的複雜性是固有的,包括問題域的複雜性、管理開發過程的困難性、通過軟件可能實現的靈活性與刻畫離散系統行為的問題,這4個方面來分析了軟件的發展一定伴随着複雜,這是軟件工程這本科學所必然伴随的一個特性。
Everything, without exception, requires additional energy and order to maintain itself. I knew this in the abstract as the famous second law of thermodynamics, which states that everything is falling apart slowly.
譯:世間萬物都需要額外的能量和秩序來維持自身,無一例外。這就是著名的熱力學第二定律,即所有的事務都在緩慢地分崩離析。
-- Kevin Kelly 《The Inevitable》
Kevin Kelly在 The Inevitable 也有提過類似的觀點,他認為世間萬物都需要額外的能量和秩序來維持自身,所有的事物都在緩慢地分崩離析。沒有外部力量的注入事物就會逐漸崩潰,這是世間萬物的規律,而非我們哪裡做得不對。
軟件架構治理複雜度 為軟件系統注入的外力就是我們的軟件架構,以及我們未來的每一行代碼。軟件架構有很多種,從最早的單體架構,到後面的分布式架構、SOA、微服務、FaaS、ServiceMesh等等。所有的軟件架構萬變不離其宗,都在緻力解決軟件的複雜性。
架構的本質
編程範式指的是程序的編寫模式,軟件架構發展到今天隻出現過3種編程範式( paradigm ),分别是結構化編程,面向對象編程與函數式編程。
結構化編程取消 goto 移除跳轉語句,對程序控制權的直接轉移進行了限制和規範面向對象編程限制 指針 的使用,對程序控制權的間接轉移進行了限制和規範函數式編程以 λ演算法 為核心思想,對程序中的賦值進行了限制和規範 面向對象的五大設計原則 S.O.L.I.D。依賴倒置限制了模塊的依賴順序、單一職責限制模塊的職責範圍、接口隔離限制接口的提供形式。
軟件的本質是約束。商品的代碼不能寫在訂單域,數據層的方法不能寫在業務層。70年的軟件發展,并沒有告訴我們應該怎麼做,而是教會了我們不該做什麼。
遞增的複雜性
軟件的複雜性不會憑空消失,并且會逐級遞增。針對遞增的複雜性有3個觀點:
模糊性創造了複雜,依賴性傳播了複雜複雜性往往不是由單個災難引起的我們可以容易地說服自己,當前變更帶來的一點點複雜性沒什麼大不了 曾經小李跟我抱怨,說這段代碼實在是太惡心了,花了很長時間才看懂,并且代碼非常僵硬,而正好這個需求需要改動到這裡,代碼真的就像一坨亂麻。我問他最後是怎麼處理的,他說,我給它又加了一坨。
編程思維論
1戰術編程
其實小李的這種做法并非是一個個體行為,或許我們在遇到複雜代碼時都曾這樣苟且過,John教授這種編程方法稱之為“戰術編程”。戰術編程最主要的特點是快,同時具備如下幾個特點。
當前一定是最快的不會花費太多時間來尋找最佳設計每個編程任務都會引入一些複雜度重構會減慢當前任務速度,所以保持最快速度 @HSFProvider(serviceInterface = AgnDistributeRuleConfigQueryService.class) public class AgnDistributeRuleConfigQueryServiceImpl implements AgnDistributeRuleConfigQueryService { @Override public ResultModelAgnDistributeRuleConfigDto queryAgnDistributeRuleConfigById(String id) { logger.info(queryAgnDistributeRuleConfigById id= ResultModelAgnDistributeRuleConfigDto result = new ResultModelAgnDistributeRuleConfigDto if(StringUtils.isBlank(id)){ result.setSuccess(false); result.setErrorMsg(id cannot be blank return result } try { AgnDistributeRuleConfigDto agnDistributeRuleConfigDto = new AgnDistributeRuleConfigDto(); AgnDistributeRuleConfig agnDistributeRuleConfig = agnDistributeRuleConfigMapper.selectById(id); if(agnDistributeRuleConfig == null){ logger.error(agnDistributeRuleConfig is null result.setSuccess(false); result.setErrorMsg(agnDistributeRuleConfig is null return result } this.filterDynamicRule(agnDistributeRuleConfig); BeanUtils.copyProperties(agnDistributeRuleConfig, agnDistributeRuleConfigDto); result.setSuccess(true); result.setTotal(1); result.setValues(agnDistributeRuleConfigDto); } catch (Exception e) { logger.error(queryAgnDistributeRuleConfigById error, result.setSuccess(false); result.setErrorMsg(e.getMessage()); } return result; } }
我們看上面這段代碼,是一段查詢分發規則的業務邏輯。雖然功能能夠work,但不規範的地方其實非常多
Facade層定義全部邏輯 - 未做結構分層業務與技術未做分離 - 耦合接口信息與業務數據Try catch 滿天飛 - 缺少統一異常處理機制沒有規範化的日志格式 - 日志格式混亂 但不可否認,他一定是當前最快的。這就是戰術設計的特點之一,永遠按當前最快速交付的方案進行推進,甚至很多組織鼓勵這種工作方式,為了使功能更快運作,隻注重短期收益而忽略長期價值。
2戰術龍卷風
Almost every software development organization has at least one developer who takes tactical programming to the extreme: a tactical tornado.
譯:幾乎每個軟件開發組織都有至少一個将戰術編程發揮到極緻的開發人員:戰術龍卷風。
- John Ousterhout 《A Philosophy of Software Design》
将戰術編程發揮到極緻的人,叫戰術龍卷風。戰術龍卷風以腐化系統為代價換取當前最高效的解決方案(或許他自己并未覺得)。戰術龍卷風也有如下幾個特點:
是一位多産的程序員,沒人比龍卷風更快完成任務總能留下龍卷風後毀滅的痕迹留給後人去清理是真的很卷 一些組織甚至會将戰術龍卷風視為英雄,為什麼能幹得又多又快?因為他将成本放到了未來。軟件工程最大的成本在于維護,我們每一次代碼的改動,都應該是對曆史代碼的一次整理,而非單一的功能堆積。龍卷風能赢得現在,但終将失去未來,而這個失敗的未來或許需要全團隊與他一起買單。
3戰略編程
John教授提出與戰術編程相對的是戰略編程,戰略編程更注重長期價值,不滿足于功能work,緻力于制作出色的設計,以滿足對未來擴展的訴求(注意,不要過度)。戰略設計有如下4個特點
工作代碼遠遠不夠引入不必要的複雜度不可接受不斷對系統設計進行小幅改進投資心态(每位工程師都需要對良好的設計進行連續的少量投資 10~20%) John Ousterhout教授在 A Philosophy of Software Design 書中提到了戰略設計與戰術設計的總成本投入。随着時間的流逝,戰略設計可以有效控制軟件成本,但戰術設計會随着時間的推移線性遞增。這與Martin Fowler在 Patterns of Enterprise Application Architecture 這本書中所提的關于數據驅動與領域驅動關于複雜度的治理是同樣的含義,要緻力于長期的價值投資。
系統的困境與演進
沒有系統是天然複雜的,為了快速完成任務不斷引入新的複雜度至系統逐漸腐化,無限增長與無限傳遞的複雜度讓軟件需求越來越難“快速完成”。當有一天我們意識到系統的複雜性時再試圖通過戰略設計進行軟件的叠代,你會發現舉步維艱,一處很小的修改需要投入大量的基建修複,最終我們不得不向成本低頭,不斷再通過戰術設計無限的苟且。
A condition that is often incorrectly labeled software maintenance. To be more precise, it is maintenance when we correct errors; it is evolution when we respond to changing requirements; it is preservation when we continue to use extraordinary means to keep an ancient and decaying piece of software in operation. Unfortunately, reality suggests that an inordinate percent- age of software development resources are spent on software preservation.
譯:我們總是說我們需要“維護”這些老系統。而準确的說,在軟件發展過程裡,隻有我們修正錯誤時,才是維護;在我們應對改變的需求時,這是演進;當我們使用一些極端的手段來保持古老而陳腐的軟件繼續工作時,這是保護(苟且)。事實證明我們更多的時間是在應對最後一種狀況。
- Grady Booch 《Object-Oriented Analysis and Design with Applications》
如同Grady Booch在 Object-Oriented Analysis and Design with Applications 中所提到的觀點,當我們使用一些極端的手段來保持古老而陳腐的軟件繼續工作時,這确實是一種苟且。我們小心翼翼、集成測試、灰度發布、及時回滾等等,我們沒有在“維護”他們,而是以一種醜陋的方式讓這些醜陋的代碼繼續能夠成功苟且下去。當代碼變成意大利面條時,将幾乎是不可能修複,成本将成指數倍增長,并且似乎我們的系統已經存在這樣的代碼,并且可能還在持續增加中。
架構僞論 在架構設計中,總有一些軟件工程師所堅信的詩和遠方,但到不了的烏托邦不一定就是遙不可及的美好聖地,實則也可能是對系統無益甚至有害的架構設計。這裡列舉其中2條可能存在的架構僞論。
1、好的代碼自解釋
Comments do not make up for bad code
譯:注釋不是對劣質代碼的補救
- Martin Fowler 《Clean Code》
Martin Fowler在 Clean Code 書中提到注釋不是對劣質代碼的補救,以前我也一直堅信如果代碼足夠好是不需要注釋的。但實則這是一個僞命題,John教授這麼評價它 ‘good code is self-documenting’ is a delicious myth。
/** * 批量查詢客戶信息 */ public ListCustomerVO queryCustomerList(){ // 查詢參數準備 UserInfo userInfo = context.getLoginContext().getUserInfo(); if(userInfo == null || StringUtils.isBlank(userInfo.getUserId())){ return Collections.emptyList(); } LoginDTO loginDTO = userInfoConvertor.convert(userInfo); // 查詢客戶信息 ListCustomerSearchVO customerSearchVOList = customerRemoteQueryService.queryCustomerList(loginDTO); IteratorCustomerSearchVO it = customerSearchVOList.iterator(); // 排除不合規客戶 while(it.hasNext()){ CustomerSearchVO customerSearchVO = it.next(); if(isInBlackList(customerSearchVO) || isLowQuality(customerSearchVO)){ it.remove(); } } // 補充客戶其他屬性信息 batchFillCustomerPositionInfo(customerSearchVOList); batchFillCustomerAddressInfo(customerSearchVOList); return customerSearchVOList; }
這段代碼我們可以很輕松的在5秒内看明白這個函數是做什麼的,并且知道它内部的一些業務規則。無限的私有方法封裝會讓代碼鍊路過深,無限類的拆解會造成更多網狀依賴,至少有3點内容,讓我們絕不能抛棄注釋。
無法精準命名命名的含義是抽象實體隐藏細節,我們不能在一個名字上賦予它全部的信息,而必要的注釋可以完美的進行輔佐。設計思想的闡述代碼隻能實現設計不能闡述設計,這也是為什麼一些複雜的架構設計我們需要文檔的支撐而非代碼的‘自解釋’,在文檔與代碼之間的空隙,由注釋來填補。母語的力量這點尤其适合我們中國人,有時并不是因為注釋少代碼多,所以我們下意識會首先看代碼。而是我們幾十年感受的文化,讓我們對中文與ABC具有完全不一樣的感觀。 2、永遠追求最優雅
雷布斯曾自誇自己寫的代碼像詩一樣優雅,追求優雅的代碼應該是每個軟件工程師的心中的聖地。但有時存在一些不優雅,存在一些‘看似不合理’并不代表就不對,反而有時在追求更優雅的路上我們持續跑偏。
The goal of software architecture is to minimize the human resources requiredto build and maintain the required system.
譯:軟件架構的終極目标是,用最小的人力成本來滿足構建和維護該系統的需求
- Robert C.Martin 《Clean Architecture》
Robert C.Martin在 Clean Architecture 一書中提到了架構終極目标,用最小的人力成本來滿足構建和維護該系統的需求。架構始終是我們解決複雜度的一個工具,如果當前系統并不複雜,我們不需要為了所謂的優雅去過分改造與優化它,持續将成本置在一個較低水位,就是軟件最好的解決辦法。
業務簡單的系統不應用DDD架構,弱交互場景也無需進行前後端分離,哪怕是鄧總設計師在規劃新中國的發展上,也是制定了一套‘中國特色社會主義’制度。不要盲從一些教條的觀念,選擇适合自己的,控制在可控制範圍内,既不過度也不缺失。畢竟沒有絕對的優雅,甚至沒有絕對的正确。
寫在最後 很多人認為做業務開發顯得沒那麼有挑戰性,但其實正好相反。最難解決的bug是無法重現的bug,最難處理的問題域是不确定性的問題域。業務往往是最複雜的,面向不确定性設計才是最複雜的設計。軟件工程學科最難的事情是抽象,因為它沒有标準、沒有方法、甚至沒有對錯。如何在軟件固有的複雜性上找到一條既不過度也不缺失的路,是軟件工程師的終身課題,或許永遠也無法達到,或許我們已經在路上了。
參閱書籍:
- 《A Philosophy of Software Design》
htt
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!