在 「軟件設計要素初探」 一文,嘗試從軟件設計的總體角度,綜合討論了軟件設計的各類要素。本文主要探討一些稍小的設計子主題,主要包括:錯誤處理、結構性難題、總體與兼容、設計取捨、設計與重構、設計與質量、設計與細節、維護與擴展、測量技術。
html
錯誤處理關乎系統的健壯性,且是全局性設計問題。一個總體的錯誤處理架構主要包括兩部分:前端
參數的嚴格校驗、規範而易於理解的錯誤碼和錯誤消息、無遺漏的異常捕獲和轉譯、警告和錯誤日誌輸出;python
一致的錯誤處理機制、不一樣級別錯誤的處理策略。編程
第一部分並不須要複雜的技術,更可能是規範、細心;第二部分則須要有總體的考慮,錯誤處理策略是採用忽略、捕獲並處理、捕獲並轉譯、直接向上傳遞,須要仔細的思考和權衡。設計模式
通常來講,關鍵環節出錯,快速返錯,毫不姑息和存僥倖心理;非關鍵環節出錯,考慮是否能夠忽略、設置默認值、兼容處理;依賴的底層服務出錯,進行捕獲並轉譯異常,並保留底層出錯緣由供問題排查;在高層更能合適地處理,則採用向上傳遞異常。不管採起何種策略,打印合理的錯誤日誌是必要之徑。 可參閱文章:「如何使錯誤日誌更加方便排查問題」安全
若過程當中涉及未完成的部分數據存儲,必要的時候須要回滾或補償措施、自動恢復機制。寧肯爲空,也不返回錯誤數據,誤導用戶。多線程
錯誤處理體現了思考問題的縝密程度。一個流程裏,有哪些可能出現的錯誤和異常場景? 對於每種錯誤和異常該如何有效地處理? 須要多仔細推敲,這可比智力題更有挑戰。架構
系統健壯性主要可分爲流程健壯性和數據健壯性。流程健壯性是系統的某一環節出錯瞭如何處理,是設計錯誤處理的一般考慮。數據健壯性是系統健壯性的另外一種情形。在訂單導出中尤其重要。因爲歷史遺留問題,必然有一些訂單有髒數據,有一些訂單的數據不符合當前設計。這樣,在訂單導出時,要保證兩個:a. 字段之間的導出相互獨立,一個字段出錯不影響其餘字段導出; b. 不一樣的訂單和商品之間的信息導出相互獨立,一個商品的出錯不影響其餘商品的導出;c. 髒數據也能合理顯示。 不要吝嗇 try-ctch 語句,捕獲異常後不要吝嗇加上一句 logger.warn 或 logger.error. 實際能夠編寫一個通用函數來捕獲各類異常。Python可參閱文章:「python使用裝飾器捕獲異常」, Java 可以使用函數接口來實現相似功能。併發
防護式編程是保證系統健壯性的核心技巧。不假定系統的流程或數據是可靠的,而是假定它不可靠或調用失敗或數據爲空會怎樣。對空值和臨界值敏感,對於預計到的情形,寧肯if-else代碼難看一點(可重構優化),也不要拋出NPE不利於問題排查;對於操做,寧肯報錯,也不作出根據錯誤假定做出錯誤操做。函數
軟件開發中遇到的技術問題一般是結構性難題。現有的設計,每每建立一種相對可擴展的空間結構,便於維護者更好地實現常見需求和功能。與此同時,設計也創造了不可見的束縛。當需求難以知足時,就是發現設計束縛的最佳時機。
例子一,順序結構簡單可控,可是不能充分利用多核的潛力,一個核心工做的時候,其餘核心乾等着,這是束縛; 併發結構可同時釋放多個核心的能力,束縛是難以控制多個核心同時工做的時序;對等應用結構可以避免單點故障,但是當多個對等應用同時操做一個不可重複的互斥資源時,則必須進行併發同步控制。
例子二,針對一對一的實體關聯的設計,簡單易用易理解;但是當實體關聯擴展到一對多或多對多的時候,就顯得不靈活了。 在設計之初,及時與產品溝通和仔細斟酌,爲一對多或多對多的場景要預留餘地。好比周期購發貨。初期設計是一個訂單一個週期購商品,每一個訂單有多個期次的發貨(期次對應訂單維度);如今須要改形成一個訂單多個週期購商品,每一個商品都有多個期次的發貨(期次對應商品維度)。若是事先已經考慮到一個訂單對應多個週期購商品的發貨,而且期次對應商品,那麼這次就比較天然地兼容多商品多期次發貨了。
例子三,一個字段的同步一般來源於一個表,但是有的字段來源於多個表,須要根據不一樣業務場景進行覆寫。這就須要設計一個更具通用和可擴展的同步覆寫機制來處理。或者從另外一個角度思考,若不但願把同步作得太複雜,則須要應用程序提供一個可擴展的數據聚合機制,從不一樣的數據源中獲取和聚合數據。訂單導出其實是一個數據聚合應用,須要從多個分散的業務表中獲取數據,而這些業務表每每不是爲了聚合而設計的。所以,導出的前置工做是設計通用可擴展的數據同步機制將所需數據從業務表同步到數據存儲中心,而後設計通用可擴展的數據聚合機制獲取數據,格式化並輸出。
例子四,訂單導出應用原來只支持固定字段的商品維度的CSV導出,如今須要支持指定字段集合的訂單(須要去重)和商品兩個維度的CSV/Excel導出,勢必要重構原來的代碼結構,支持更靈活的導出能力。可使用策略模式來分離和組合不一樣的維度和格式導出。
經常爲了簡便而硬編碼。硬編碼當然能解一時之需,卻容易誘發設計束縛。
幾乎全部設計結構都具有某種優勢,同時也具有某種束縛力。要讓應用從束縛力中解脫出來,則必須藉助多種結構的組合,取長補短,才能實現最終的大設計。
總體與兼容是結構性難題的常見情形。軟件功能之間每每不是孤立的。好比訂單管理中,商家根據各類條件篩選後導出待發貨的訂單,而後使用批量發貨的功能將訂單發貨,接着在訂單詳情頁查看訂單是否如預期發貨,或者從新再導出一次看看是否已經發貨。發貨後還可能修改物流。這樣,訂單搜索/導出/發貨/詳情,其實是一個組合操做場景。當要支持新業務好比周期購時,這些都是必須總體化考慮的。單從某個功能模塊的實現來講並不困難,困難的是將四個組件的功能服務串聯成一個緊密聯繫的邏輯嚴謹的總體。
兼容現有系統則是開發者最頭疼的任務類型,尤爲是系統已經發展到比較大的時候。事實上,每次代碼變動都是一次現有系統兼容,只是有些兼容看起來使人深爲苦惱甚至「不堪回首」。一般是因爲原有設計對關注點沒有解耦清晰,沒有考慮到新需求的狀況,沒有預留足夠的餘地,兼容起來必須努力尋找空間,更像是一種英勇的突圍行動。兼容系統的比較好的一種方式是,先分析清楚原有系統的業務邏輯和設計思路,而後嘗試從一種更通用的角度去容納原有的設計,有時會須要改動很多,須要可靠的迴歸測試來護航。所以,測試用例的充分覆蓋程度,每每決定了系統可否大膽進行重構,實現更佳的更安全的設計優化,而不是簡單增長一坨條件分支語句。
設計須要大量取捨,須要優秀的判斷力。理解是一回事,設計和取捨是另外一回事。可以是一回事,應該是另外一回事。設計應該儘量簡潔實用。哪些須要進行權衡呢?我的遇到的情形主要有三點可參考:
(1) 爲了更優的體驗而把事情弄複雜。現代產品設計強調用戶體驗,有時用力過猛,反倒容易忽視簡單性帶來的隱形良好體驗。人容易犯錯。複雜而完美的事情,儘管每每蘊含了不少「貼心的思考」,可這些「貼心的思考」在真實場景未必成立或只是偶爾出現,或者受到更多限制而沒法施展,甚至起到反制做用,使人困擾和沮喪。相反,簡單的事物,儘管不完美,卻預留了不少空間,人們總能想到「奇妙方法」應對現實的阻撓因素並廣爲傳播,而這些「奇妙方法」反而成了事實上的解決方案。所以,產品設計應儘可能簡單而預留餘地,流程儘可能直線式單一化且短小,消減分支和重試,減小誘發出問題的潛在因素。而對於系統設計而言,要儘可能作到關注點正交分離解耦,更好地支持產品的靈活性;而即便可以作到產品的靈活性,也要仔細斟酌是否必須作到如此。
舉個例子,使用第三方配送,容許商家一個訂單多個包裹配送、每一個包裹容許第三方配送失敗後切換快遞配送、容許商家屢次拆分包裹和重組包裹。這些要求誠然是比較合理的,也足夠靈活,卻忽視了兼容現有系統帶來的連鎖問題:快遞配送後有修改物流,結合容許屢次拆分和重組包裹,意味着商家可反覆修改物流,會增長髮貨和顯示覆雜度;商家拆分和重組包裹後,基於以前商品組合計算的支付金額可能不正確;前端展現上須要作很多兼容工做,容易出BUG。所以,靈活性當然很美好,實際上一則商家未必用得上那麼多功能,二則某個功能失敗後會衍生出更多的組合場景難以覆蓋徹底,三則須要複雜度較高的設計和大量的研發測試成本,四則出了問題難以排查和修復,耗費商家、客服和技術研發的時間。實際上,一個訂單有多個包裹是直觀無疑的認識,而大多數場景只須要一個包裹配送;「容許第三方配送失敗後切換快遞配送」是產品針對「配送員不接單如何處理」的依託現有功能的解決方案,並非原始需求,儘管如此仍是合理的;「容許商家屢次拆分包裹和重組包裹」 是產品針對問題「多包裹配送及包裹配送失敗如何處理」想出來的解決方案,也不是原始需求。 和產品同窗對需求時,要仔細識別哪些是原始需求和問題,哪些是用於解決需求和問題的帶有方案性質的僞需求,還要仔細識別出兼容現有系統設計實現所引起的潛在問題。需求和問題是產品出,方案是研發出而通過產品承認。上線後,研發和產品一塊兒關注效果和反饋,並改進優化。
(2) 過分設計可擴展性。預先思考過多,想作的更靈活,結果實際發展方向並不是所料,增長了系統設計複雜度、研發成本卻沒有收到實效。作訂單導出時,爲了根據不一樣業務更靈活地輸出報表字段,將報表字段按照業務劃分,分別定義主字段集和不一樣業務須要的子字段集,而生成報表的最終字段集須要主字段集合與若干子字段集組合而成,略顯複雜,實際沒用到。而按照指定字段集輸出,倒是正確的思考方向。凡基礎而必要的老是正確的方向。
真實需求是分別按照訂單維度和商品維度來進行導出,不一樣業務可指定不一樣維度以及所需的報表字段集。新版設計是將訂單維度的字段與商品維度的導出字段分別定義,將字段格式化定義與輸出字段集指定解耦,將訂單維度導出與商品維度導出作成兩種不一樣的策略,這樣,不管按照訂單維度仍是商品維度,不管指定什麼字段集合,均可以按照指定需求輸出報表。這是按照真實需求作出的有效設計。有效的設計應當是根據需求對原有設計結構進行重構優化,獲得更靈活的設計,而不是開始就想不少,作得複雜。
(3) 設計不可偷懶。儘管設計要提倡節制,但是也不能偷懶,在必要的地方設計不足。一旦設計不足,很快就會受到「懲罰」。一個產品初始誕生時,經常從最簡單的情形考慮,好比一對一的情形。而設計卻要考慮,未來是否會發展到一對多的情形,並留下設計餘地。若是心存僥倖以爲不太會有這種可能或者掩耳盜鈴,那麼現實不久後就會來一次生動的教訓課。
「如無必要,勿增實體」,或可做爲設計基本原則之一。設想去掉它,是否是會運轉得很是艱難或形成資損?
好設計不是一蹴而就的。好設計是可演化的。需求與問題是好設計的催化劑。持續地對設計進行小步重構優化,使設計更有活力和適應力。
對設計作小步重構,首要是抽離出關注點。推敲關注點的行爲和目標,抽象成適當的接口並實現成小組件。抽離出關注點後,須要將關注點有序地融入到總體流程中。能夠想象,應用由許多小組件組成,每一個小組件實現某個關注點的接口;而應用經過若干主流和支流串聯起全部的小組件而實現其功能和服務。簡而言之,「關注點-接口-組件-流-應用」。設計軟件有時像設計遊樂園,先構思各類微小景緻(微組件),而後設計四通發達曲徑通幽的小路(流)串聯起全部的微小景緻。
好比,原來只支持商品維度的CSV導出,如今須要支持訂單維度和商品維度的CSV/Excel導出。首先抽離關注點。訂單/商品維度是數據維度,而CSV/Excel是輸出維度。數據維度關注將報表原始數據列表轉化爲對應報表字段的報表行列表;輸出維度關注生成報表文件以及將報表行列表添加到報表文件中。根據關注點定義相應的維度策略接口和文件輸出策略接口,而後分別實現訂單/商品維度報表行生成組件和CSV/Excel文件輸出組件,最後在導出流程中根據參數選擇相應的策略來調用相應組件的接口,替換原來的實現。分離關注點,並定義關注點的合適接口,是設計與重構的基本功。
設計重構主要包含兩個層面: 代碼層面,使用設計模式進行對象交互結構的重構,使功能和服務實現更具柔性,容易修改和擴展;模型層面,使用深化和顯化領域概念,優化存儲設計,使得領域模型更加清晰。
好設計不只僅是知足軟件功能,還必須知足預期軟件質量指標。軟件質量指標可參閱文章:「Web服務端軟件的服務品質概要」。在進行軟件設計時,務必明確軟件預期的質量指望,並在功能實現以後進行衡量和評估。
問題是一系列關注點的聚合體,細節則是具體的小的關注點,設計則是針對系列關注點的一致性處理機制。「關注點分離」,是設計的基本原則。
爲何會產生「細節」呢?當設計可以覆蓋到問題的全部關注點時,細節就被囊括到設計所考慮的範圍內;而一旦問題的關注點有所變化發展時,設計就沒法覆蓋到全部關注點,就產生了所謂的「細節」以及特殊處理,特殊處理帶來的就是一堆堆的條件分支語句。爲何設計沒法覆蓋到全部的關注點呢? 由於設計經常是一些通用套路,而這些套路每每是比較大的聚合體,很實用但未必靈活。 理想狀況下, 設計也應該包含一系列正交的小的子設計,經過子設計的組合來構建更大的設計,覆蓋更多的關注點,這樣,細節及特殊處理就會更少, 代碼也更具可維護性。
所以,從細節反推設計的不足,可促進設計的優化,覆蓋更多的關注點。
可維護、可擴展是軟件可以持續健康發展的重要質量屬性。可維護的要點是,持續對代碼進行小步重構精練;可擴展的要點就是:識別、分離和組合關注點。在可擴展的基礎上,進一步可實現可配置化。「Java8Map示例:一個略複雜的數據映射聚合例子及代碼重構」展現了一個例子,經過分離和配置關注點(經過itemIdConf配置來關聯各個表的對應數據),消除了一大段的if-elseif-elseif 語句的,開心!使用適當的配置,輔以枚舉、Map,可有效改善或消除代碼裏大段的if-else, switch語句。
從結構角度看軟件,軟件就是一種虛擬建築。牢固的實體建築須要仔細的測量和組裝,持續穩定運行的虛擬建築也須要仔細的測量。測量指標是設計可行性和可靠性的關鍵衡量。缺失測量指標的設計是不可信的。性能(響應速度與吞吐量)、佔用資源(CPU與內存)、穩定性(成功率)、可用性(故障時長及比例)、峯值與平均負載能力(壓力錶現)是常見的軟件測量指標。
最基本的方式是,循環調用服務接口若干次,統計平均/最大/最小響應速度;多線程併發調用服務接口,設置併發數及佔用資源上限,測量其吞吐量;持續指定時間多線程併發調用服務接口,統計成功次數及成功率。壓測則須要找QA同窗幫忙部署正規的壓測環境,而後使用專門的工具和方法進行測量。