背景
上周開發了一個需求,發現一個歷史功能,從產品和技術代碼的角度看,將簡單的事情變得復雜。這一經歷再次深化了我對一個核心理念的認識:簡化復雜性是產品設計和軟件開發中永恒的挑戰。我們必須不斷努力,將復雜的邏輯轉化為直觀、易用的用戶功能,并將冗長、難以維護的代碼結構變為簡潔、效率高的形式。
在《人月神話》中作者提到,軟件開發的復雜度可以劃分為本質復雜度和偶然復雜度。本質復雜度它是一個客觀的東西,跟你用的工具、經驗或解決渠道都沒有任何關系。而偶然復雜度是因為我們在處理任務的時候選錯了方向,或者使用了錯誤的方法。
作為工程師,我們的追求不僅僅局限于代碼的編寫。更深層次的,我們探索的是如何對抗軟件本身產生的復雜度,如何將繁雜的需求轉化為簡潔、優雅的解決方案。
不單單是程序員,任何化繁為簡的能力才是一個人功力深厚的體現,沒有之一。越簡單,越接近本質。這個“簡單”指的是整體的簡單,而不是通過局部的復雜讓另一個局部簡單。
附:需求案例復雜點 1)業務產品設計方面:Promise 業務類型(比如生鮮時效、航空時效、普通中小件時效等)與單據類型、作業類型之間存在一系列復雜的轉換關系。但這幾個類型本質是一樣的,沒必要轉換,術語統一,對業務使用來說也簡單。 2)技術代碼方面(組內同學CodeReview發現的):代碼方法“副作用”(side effect),即方法除了返回值之外,還通過修改某些外部狀態或對象來傳遞信息。比如filterBusinessType方法的主要作用是返回一個int類型的值,但它也修改了入參的response對象作為一個副作用,外部鏈路會使用reponse對象屬性值。并且代碼內部調用鏈路復雜,對于新人來說成本較高。為了確保清晰理解這些關系,并有效地進行代碼維護,特意對這些關系及代碼鏈路進行了詳細的梳理。
一、為什么要簡單?
為什么我們要追求簡單性?不應該是復雜,才能顯得技術牛嗎?
對應簡單,各有個的說法,個人理解如下:所見即所得
1.比如你架構圖別人一看就明白這是干什么的,系統之間如何交互,模板之間如何交互。
2.比如定義API 別人一看文檔就明白功能職責,請求入參,出參含義,有基本的計算機知識人都能看明白,這叫所見即所得。
3.比如新人在1周左右就能快速更改代碼,而不是要記住代碼各種注意事項,盡可能減少用戶的學習曲線和理解成本
接下來從本次需求的復雜案例著手,引入自己的一些思考
二、案例詳細
1)產品設計
1.1)現狀
Promise業務類型 > 單據類型 > 作業類型 各種轉換關系
1.業務類型 轉換單據類型(1 VS 1)
2.單據類型 轉換為 作業類型:根據單據類型找到 倉、干支線、本地 作業類型
3.倉&干支線&本地 作業類型:
1.2)思考點
1.一對一映射的簡化
“業務類型&單據類型 其實是1V1映射”,這表明系統當初設計時考慮了業務類型和單據類型之間的直接關聯。如果這種一對一的關系確實存在,那么單據類型可能是一個冗余的概念,因為每個業務類型已經隱含了單據類型的信息。簡化模型,去除冗余的單據類型,可以減少系統的復雜性,并可能簡化數據庫設計和代碼實現。
1.概念統一
“作業類型本質還是業務類型”,這意味著在不同的上下文中可能使用了不同的術語來描述相同的概念。在編碼和產品設計中,使用統一的術語可以減少混淆,提高團隊成員之間的溝通效率,并使得新成員更容易理解系統。
1.維度劃分
將業務類型進一步細分為“倉、干支線、本地維度”,這表明系統有不同的操作維度或者分類標準。這種維度劃分有助于在不同層面上組織和處理業務邏輯。
2)代碼問題
2.1)內部鏈路太長
業務類型邏輯入口之一:getBusinessTypeInfoForAll > getBusinessTypeInfo > getOrderCategoryNew > obtainOrderCategoryByCode > filterBusinessType
??
在我們的代碼庫中,上面關鍵的五個方法被多個調用入口所使用,這種情況使得管理這些入口變得極為棘手。由于調用點的廣泛分布,理解代碼的影響范圍變得復雜,難以一目了然地掌握。此外,這種做法也顯露出我們的代碼缺乏清晰的分層架構。這一原則的缺失,不僅使得現有代碼難以維護,也給未來的功能擴展和迭代帶來了不必要的復雜性和風險。
2.2)副作用
在Java 編程語言中,術語“副作用”(side effects) 指的是一個函數或表達式在計算結果以外對程序狀態(如修改全局變量、改變輸入參數的值、進行I/O 操作等)產生的影響。
?
如下filterBusinessType方法的主要作用是返回一個業務類型int類型的值,但它也修改了傳入的response對象的A值作為一個副作用。在外面鏈路使用了A屬性值做邏輯判斷
?
副作用問題:在filterBusinessType方法中如果是在response之前return了數據,從方法角度看不出問題,但整個鏈路會出現問題。
?錯誤寫法
public int filterBusinessType( Request request,Response response) { if(...){ return ... } boolean flag = isXXX(request, response); }
?正確寫法
public int filterBusinessType( Request request,Response response) { /** * 切記:return必須在下面這行代碼(isXXX方法)后面,因為外面會使用response.A()來判斷邏輯 * 你可以理解本filterBusinessType方法會返回業務類型,同時如果isXXX方法會修改response.setA()屬性 */ boolean flag = isXXX(request, response); if(...){ return ... } }
2.3)思考點
思考點1:代碼鏈路太長
內部代碼鏈路太長是一個常見的維護問題,通常源于缺乏良好的模塊化和抽象設計。以下是一些思考點:
1.避免過度抽象:雖然抽象可以幫助簡化代碼,但過度抽象反而可能會增加復雜性。確保你的抽象層次是合理的,并且每個抽象都有明確的目的和價值。
2.分層架構:將代碼按照邏輯和職責分成不同的層次,每一層只對其下一層有依賴性,而不需要知道更深層次的實現細節。這樣可以減少代碼之間的耦合度,簡化內部鏈路。
3.合并重復代碼:如果發現多個方法都在執行類似的操作,嘗試合并這些重復的代碼段,創建一個通用的方法來處理它們。這樣可以減少代碼的總量,提高代碼的可讀性和可維護性。
4.團隊共識和標準:與團隊成員討論并達成共識,制定一些編碼標準和最佳實踐,以便在日常開發中就能夠遵循簡潔性原則,避免產生新的長鏈路。
思考點2:副作用
1)注意事項
1.將副作用明確化:如果一個方法有副作用,應該在方法的名稱、文檔或使用方式中明確指出。這樣可以幫助其他開發者更好地理解該方法的行為。
2.避免使用靜態變量:靜態變量可以被多個線程或方法共享,容易引起副作用問題。除非有明確的理由,否則應盡量避免使用靜態變量。
3.完全消除副作用可能是不現實的,尤其是在需要與外部交互的應用程序中。關鍵是要理解副作用的存在,并采取合適的策略來管理和控制它們。
2)Java的設計約定鼓勵我們遵循以下原則:
1.單一責任原則:每個方法或類都應該有單一的責任或功能。
2.最小驚奇原則:方法的行為應該符合預期,避免出現意外的副作用。
在這個例子中,filterBusinessType方法既返回一個整數結果,又更新了response對象,這違反了單一責任原則和最小驚奇原則。因為調用者可能會預期這個方法只會過濾業務類型,而不清楚它還會修改response對象。
3)如何規避這種現象
為了避免這種情況,可以采用以下幾種策略:
1.分離關注點: 可以將獲取業務類型和響應設置分離成兩個不同的方法。這樣,調用者就可以清晰地看到每個方法的職責。
public int filterBusinessType(String logPrefix, Request request) { // 過濾邏輯... int businessType=...; return businessType; } public void setResponseData(int filterResult, Response response) { // 根據過濾結果設置響應數據... response.setFilteredData(...); }
1.返回復合對象(上下文context): 如果業務類型結果和響應數據是緊密相關的,可以考慮創建一個包含這兩個信息的復合對象,并將其作為方法的返回值。
public FilterResultAndResponse filterBusinessType(Request request) { // 過濾邏輯... int result=...; Response response=new Response(); response.setFilteredData(...); return new FilterResultAndResponse(result, response); } class FilterResultAndResponse { private int filterResult; private Response response; public FilterResultAndResponse(int filterResult, Response response) { this.filterResult = filterResult; this.response = response; } // Getters and setters for filterResult and response }
總的來說,副作用有時候是不可避免的,但我們可以通過以上方法來規避和管理它們,寫出更可靠、更易于維護的代碼。
三、案例解決方案
回到本文開頭說的產品和技術復雜性案例,考慮到產品和技術影響promise時效內核最底層邏輯范圍。我是采取保守的策略:維持現狀不變。這是因為對核心邏輯進行改動會帶來廣泛的影響和較高的風險,可能會牽扯到整個系統的穩定性和一致性。然而,這并不意味著我們放任復雜性存在。相反,我做了以下兩點改進措施:
1.增加注釋和注意事項:在代碼中添加詳細的注釋和注意事項,幫助團隊其他開發者理解這部分代碼的工作原理和潛在風險。這樣可以降低新成員上手的難度,并且減少在維護或擴展時出現錯誤的可能性。
2.團隊分享和知識傳遞:我將分享這個案例和相應的經驗教訓,向團隊成員解釋為什么在這個特定情況下我們選擇了保持現狀不變,以及如何在未來的需求、架構設計和代碼編寫中更好地管理復雜性和副作用。通過這種方式,我們可以共同學習和成長,避免在類似情況下重蹈覆轍。
這兩種方法雖然不能立即簡化復雜性,但它們可以提高代碼的可讀性和可維護性,減少長期的技術債務。同時,團隊分享也能促進知識共享和團隊協作,讓大家知道什么是正確的做法,幫助我們在面對類似挑戰時做出更明智的決策。
四、如何做到簡單--思考點
KISS 原則是指在設計當中應當注重簡約的原則。總結工程專業人員在設計過程中的經驗,大多數系統的設計應保持簡潔和單純,而不摻入非必要的復雜性,這樣的系統運作成效會取得最優;因此簡單性應該是設計中的關鍵目標,盡量避免不必要的復雜性。
1)產品設計
作為技術從業者,我們常常需要從用戶(無論是面向C端消費者還是面向企業內部的產品)的視角出發,來探討產品設計中的簡潔性原則。以下是我的一些觀點,說的并不一定對,但愿能為您提供一些啟發。
以用戶為中心
?深入理解用戶需求:深刻洞察用戶的核心需求和痛點,用客觀數據驅動決策,而不是單憑個人直覺。
?簡化用戶旅程:力求打造一個直觀的用戶旅程,盡量減輕用戶的決策壓力和學習負擔。一個優秀的界面應該是清晰、易懂的,使用戶能夠毫不費力地完成所需任務。
減法設計
?在設計每一個功能環節時,我們需要反復自問:這個功能是否真正必要?如果它的缺失不會損害用戶體驗,那它很可能是多余的。
直觀交互
?設計時應確保控件和操作邏輯能夠自然地映射其功能,使用戶能夠直覺地理解產品的使用方法。
持續迭代
?不斷地收集用戶畫像和分析用戶反饋,將其作為產品設計迭代的重要依據,以此不斷地精進和完善產品。
案例: 1、Alfred:這個我覺得根本無需介紹,神器,使用 macOS 的同學應該都知道。一句話來說就是,Alfred 是 macOS 上神級的效率應用,能夠在實際操作中大幅提升工作效率。
2、1Password:使用 1Password 生成并管理密碼后,就再也不用費心思去想密碼的事情了,只需要記住 1Password 主密碼就萬事大吉。 原先你需要4步驟:比如1)打開瀏覽器 2)輸入網站(或者打開收藏夾) 3)打開網站輸入用戶名密碼 4)點擊登錄 使用1Password只需要 一步到位,自動打開瀏覽器登錄相關頁面
2)架構設計
不要跟風選擇所謂高大上的技術,適合的才是最重要的。夠用+1即可。什么意思呢,就是系統目前夠用再往前走一步就可以了。至于這一步是什么?可能需要你在實踐過程中,慢慢找到你認為比較合適。很多時候,我們系統架構引入一個新框架或者新技術,它本身帶來的復雜性其實比你這個問題還要復雜。
簡化架構也是提高技術穩定性的重要步驟。一個復雜的架構可能會導致系統的各個部分難以協同工作,從而影響系統的穩定性。因此,我們應該盡量采用簡單的架構設計,使得各個部分可以更容易地協同工作。
案例:業務架構簡單化-小件日歷天數30天擴充到90天 復雜解法: 1)目前是根據業務的時效配置預計算好30天日歷,依賴N個配置(倉-干支線-本地緩存等),在現有基礎上,預計算90天日歷。 2)缺點:牽扯數據預計算N個地方改造,并且增加了數據量的存儲。改造排期長并且數據存儲成本高 簡單解法: 1)還是保持現有30天日歷的算法。第31天以后的日歷按照最后一天日歷進行復制。如果日歷計算命中集約地址(比如3天1送),過濾對應日歷即可。 2)優點:代碼改造工作量小,數據存儲成本保持不變
?
案例2:技術架構簡單化-避免過度使用技術棧 以緩存(本地、分布式緩存)為例,它的引入確實能顯著提高系統的響應速度和效率。然而,這同時也帶來了新的挑戰,如數據一致性問題和緩存策略的選擇。
??
1)數據一致性問題可能導致用戶獲取到舊的或不正確的信息。 2)而緩存策略的選擇則需要在系統資源利用和數據時效性之間找到平衡點。 為了解決這些問題,我們可能需要引入更復雜的緩存失效策略, 1)如基于時間的失效、事件驅動的失效機制, 這些策略的引入和管理本身就增加了系統的復雜性,因此在設計緩存解決方案時,我們需要仔細權衡其帶來的效率提升和潛在的復雜性增加,以找到最適合當前系統需求的平衡點。
3)最小API
對外 API 的設計決定了系統的可擴展性和兼容性。一個清晰、簡潔且易于理解的 API 設計可以減少各種交互問題。編寫一個明確的、最小的API是管理軟件系統簡單性的必要條件,我們向API消費者提供的方法和參數越少,這些API就越容易理解,就有更多的時間去完善這些方法,
將復雜的API設計簡化為更易用、更直觀的形式,以便用戶能夠更容易地理解和使用。
1.使用標準格式:遵循一致的命名約定、數據格式和錯誤處理機制。這將使API更加一致和易于使用。
2.API功能單一職責原則:在API設計中,單一職責原則也非常重要。如果一個API具有多個職責,那么它將變得復雜且難以維護。因此,建議將API拆分為多個簡單的API,每個API只負責一個特定的職責。明確其功能和用途。這將有助于確保API具有清晰的職責劃分,避免不必要的復雜性。
3.簡化參數:盡量避免使用過多的參數,而是使用簡單、易于理解的參數。如果必須使用多個參數,請確保它們之間有明確的關系。
4.提供簡潔的文檔:編寫簡潔明了的API文檔,解釋每個端點的功能、請求方法、參數和響應格式。確保文檔易于閱讀和理解,以便用戶能夠快速上手。
5.提供示例代碼:為API提供示例代碼,展示如何使用不同的請求方法和參數。這將幫助用戶更快地掌握API的使用技巧。
在軟件工程上,少即是多,一個很小、很簡單的API通常也是一個對問題深刻理解的標志。
案例1:Promise適配M系統API 背景:M系統的時效是自己計算閉環的,promise是對外統一收口時效,在M系統時效業務線上,promise只是透傳,不做任何時效邏輯 復雜解法: 1)每次M系統相關時效需求,下游M系統的API需要變更,promise也需要參與改造,改造點2個,第一個是從訂單中間件xml獲取M系統需要的參數。第二點把獲取的參數調用M系統API透傳 2)缺點:需求改造系統多,但都是轉發適配,無核心邏輯,工作量耗時長,項目排期協調,溝通成本大 簡單解法: 1)跟M系統溝通,M系統時效要的信息從X節點獲取,promise把該節點的json信息全部透傳給M系統,這樣后期需求promise不參與改造, 2)優點:從promise角度來說新需求不用改造,從M系統角度來說時效自己閉環。這是雙贏的局面,從全局來說,減少了鏈路的開發/聯調/溝通/協調成本,整個項目交互效率提升了.
案例2:?錯誤碼設計---未傳播錯誤碼 案例:外單無妥投時間,目前鏈路是A---->B---->C系統。但錯誤碼是各自封裝,沒有把根本原因傳播出去,而是各自加工,導致最終看到的原因跟真實的原因千差萬別。導致整個鏈路牽扯 業務方--->A研發---->B研發---->C研發---->C業務同事 總共5個環節,如下圖:
??
?
案例2:?錯誤碼信息--傳播錯誤碼信息 1、如果API在翻譯錯誤時,需要把底層根本原因返回上去,比如上面案例,把沒有妥投日期的根本原因【XXXXXX】周知 2、改造后鏈路 A業務方---->C業務同事 總共2個環節(改造前5個環節),因為界面提示錯誤信息,所見即所得,減少了中間環節。提升了業務效率,減少了研發內部中間環節的排查成本。
??
4)代碼簡單
編碼簡單化也是提高技術穩定性的有效方法。過于復雜的編碼可能會導致錯誤和漏洞的出現,從而影響系統的穩定性。因此,我們應該盡量使用簡單、清晰的代碼。此外,我們還應該注重代碼的可讀性和可維護性,這樣可以更容易地找到和修復錯誤。
1.遵循單一職責原則:每個函數或類應該只負責一個特定的任務。這樣可以使代碼更易于理解和維護,并減少錯誤的可能性。
2.避免冗余代碼:盡量避免重復的代碼。如果需要多次使用相同的代碼塊,請將其封裝為函數或方法,以便在需要時調用。
3.使用注釋來解釋復雜的邏輯:如果代碼中包含復雜的邏輯或算法,請使用注釋來解釋其工作原理。這可以幫助其他人更好地理解代碼。
4.將長代碼段拆分為多個小段:如果一個代碼段很長,可以考慮將其拆分為多個小段,每個小段只做一件事情。這可以使代碼更加清晰明了,并有助于調試和維護。
5.使用有意義的變量名和函數名:變量名和函數名應具有描述性,以便其他人可以快速了解其用途。
總之,編寫簡單的代碼需要考慮多個方面,包括可讀性、可維護性和可重用性等。
五、簡單原則--踐行中
我也是正在積極踐行以下原則。雖然在實踐中仍面臨挑戰,但正不斷學習和改進
1)復雜(重復)的事情簡單(工具)化
當我們面對重復而無差別的任務時,工具化的價值便凸顯出來。引入合適的工具不僅簡化工作流程,還能大幅提升效率。
對于復雜的業務邏輯,我們應致力于深入梳理和理解。詳盡的文檔是理解這些邏輯的鑰匙,它能夠將復雜性降低。
對于系統架構,我們應該梳理上下游依賴、交互、核心接口、業務場景、應急預案等,具備全局視圖
對于技術密集的代碼,充分的注釋和示例案例是必不可少的,它們是簡化理解過程的橋梁。
我們還應該將復雜的系統解構為小型、可管理的模塊,這是一個將復雜事物簡化的過程。
2)簡單的事情標準化
一旦這些復雜的系統被拆分成多個簡單的組件,我們就可以對每個組件進行定制化和標準化。
3)標準的事情流程化
這樣的標準化模塊,一旦定制完成,就能夠形成一個簡潔且固定的流程。這種流程化不僅為防止最糟糕情況的發生提供了保障,也使得任務能以統一和高效的方式運行。
4)流程的事情自動化
正如自動化測試所示,一旦流程化得以實施,自動化的基礎便已鋪墊。基于這一基礎,我們可以將復雜的任務轉化為自動化的操作,從而盡可能地減少手動干預,實現高效運作。
案例:行云部署發布上線 簡單提效快 背景:為解決用戶手動部署操作耗時高、分組多人工容易遺漏、對人依賴度高等痛點,2個以上分組,20個容器以上的應用,強烈推薦您使用【部署編排】功能,用戶可靈活制定部署策略,實現從編譯構建到實例部署的自動化運行,提高部署效率! 復雜-->簡單-->標準-->流程-->自動化:部署編排接入了豐富的原子,提供了部署策略、流量管理、編譯構建等功能,可基于這些功能進行任務排布,形成一個獨立的部署編排。部署時,只需執行此編排任務即可,解放雙手實現自動化部署!同時部署編排支持多分組同時部署。
六、總結
1.復雜的事情簡單化
2.簡單的事情標準化
3.標準的事情流程化
4.流程的事情自動化
我們先踏出第一步化繁為簡
簡化復雜性不僅能在短期內提高開發效率和代碼質量,也對產品和技術的長期價值產生深遠影響
1.當我們考慮如何簡化一個給定的任務的每一步時,這不并是在偷懶。相反,我們是在明確實際上要完成的任務是什么,以及如何更容易做到。
2.我們對某些無意義的新功能說“不”的時候,不是在限制創新,是在保持環境整潔,以免分心。
3.軟件的簡單性是可靠性的前提條件。這樣我們可以持續關注創新,并且可以進行真正的有價值的事、長期的事。
?
本文旨在拋磚引玉,僅就偶然復雜度的議題,從產品與技術的角度,分享一些關于簡單化的個人思考。希望這些初步的觀點能激發更多精彩的思考和深入的實踐。如果文中有任何不足之處,懇請各位不吝賜教,留言指正。謝謝大家的閱讀和反饋!
審核編輯 黃宇
-
API
+關注
關注
2文章
1510瀏覽量
62393 -
代碼
+關注
關注
30文章
4825瀏覽量
69039
發布評論請先 登錄
相關推薦
評論