一文探究系統分析與設計的邏輯性

一、系統分析與設計的邏輯性框架

在日常的工作中,「軟件分析」與「軟件設計」這樣的詞眼經常聽到,然而要真正理解「軟件分析」和「軟件設計」的本質是比較難的,它依賴極強的工作經驗,又加上軟件分析與設計沒有標準的程式化步驟,導致不同的人有自己不同的方法,也就造成了很多人認爲軟件分析與設計是非常「空洞」,還不如寫具體的代碼實在,而大部分的人寫的是業務型代碼,被嘲弄寫CRUD的代碼沒成就感。

軟件分析與設計如其它行業一樣,具有很強的邏輯性,沒有邏輯性支撐,很難做好事。比如寫作,有「謀篇佈局」、「起承轉合」、「遣詞造句」等這些「原則」和「方法」,沒有洞悉到這些底層邏輯,一個高手寫的文章和一個普通人寫的文章,效果是天差地別。因此,首先要講清楚的是軟件分析與設計的「邏輯性」到底是什麼,下圖是軟件分析與設計的邏輯全景圖。

1.1 方法

1.1.1 分析階段

軟件分析與設計並沒有那麼神祕,本質來講還是爲了解決現實的問題,和「醫生看病」、「工人修車」、「廚師做菜」一樣的,都需要方法作爲指導,否則沒有任何頭緒,只能抓瞎。方法是具有普適性,只是不同的行業有各自的特性,具體落地上有差異。

既然是要解決問題,那麼總得知道問題是什麼吧,就好比醫生看病,做各種檢查、化驗,都是爲了全面地瞭解疾病,所以第一步是需要定義好問題,然而當下很多人都忽略了這一步,直接上來想我要用哪種中間件、哪個框架,連問題都沒有定義好,直接想實現無疑是本末倒置。

問題是理想與實現差距的矛盾,現實是不滿足理想的訴求,因此,首先需要了解用戶的訴求是什麼,想解決怎樣的問題,這也即爲是需求。需求分析最大的挑戰是什麼是真正想要的,就好比一個病人說了一大堆的症狀,他所說的症狀表現與書本上的描述有時是有出入的,定義出真正的需求至關重要,接下來就要思考通過怎樣的方法去梳理清楚用戶需求。

用例是表達用戶使用系統實現某些目標文本化的情節描述,它強調的是用戶目標、觀點。我們應該從用戶目標的角度出發思考,也有的人稱之爲利益相關者的關注點,道理很簡單,我們做出來的軟件是爲了服務於用戶的,不是用戶想要的,就算用了多高科技的的技術也是失敗的。

用例只是一個概括的描述,因此還需要細化,一個用例包括一個或多個場景,場景是參與者與系統之間的活動和交互,比如用戶下單,有下單成功,有下單失敗兩個場景。因此到這裏還只是分析階段,分析的目的是瞭解現狀和目標,以及系統要素組成,它不關心如何實現做、如何實現。分析不僅限於軟件行業,其它的行業也是如此,只是在分析過程的程式化步驟、方法不一樣而已。

1.1.2 設計階段

當我們清楚地知道要做什麼之後,接下來要思考如何去實現,實現的途徑有很多種,如同一百個廚師做同樣的菜,做出來的效果不一樣。設計階段有兩點需要考慮的:一是如何將功能細化實現;另一點是如何更好地實現。第一點不管用什麼方法都可以實現,難的是第二點,更好地實現是需要遵循一些章法,也需要評判體系,要不然你怎麼知道好與不好呢,常見的定量評判指標有:成本、性能、可靠性、效率等,還有一類是定性的評判指標有:開放性、體驗性等。

度量的指標相對容易,就像醫生看病,看療效、看成本,軟件設計也是一樣,歸根到底是「多快好省」。然而軟件設計的章法就複雜得多,它具有很強的藝術性,正所謂「文無第一,武無第二」,比武一定可以比出個高下,誰打贏了就是勝利一方,然而比文就難了,不能說你寫的就一定比我寫的好,只是不同人的喜好不一樣而已,當然這裏指的旗鼓相當地對比,不同層次的對比一眼還是能看得出來的。

在設計階段最爲關鍵的是定義出「關鍵的技術問題」,一般分類兩類:一是站在用戶視角的設計,它重點考量的是「便捷性」和「易理解」;另一個是站在系統層面,重點考量的是「複用性」、「擴展性」和「穩定性」。貼近用戶的設計,讓用戶用最少的理解就能使用它,用戶無須感知底層複雜的設計,核心是回答用戶最樸素的原始訴求。系統層面的設計要靈活,多用組合正交設計提升系統的複用性和擴展性,在具體方案設計中要考慮到穩定性因素,比如寫日誌會帶來性能問題,這個在方案設計中就要考慮到。洞察到關鍵技術問題,並非一朝一夕就能練就成,需要在工作中大量地實踐,總結經驗,保持技術的敏感度纔行,在第二節中通過實際的案例方便大家加深理解。

1.2 工具

有了方法,接下來要有工具來幫我們更好地做事,與方法對應的,我們軟件設計的工具是UML,接下來介紹UML中常用的圖。

1.2.1 活動圖

軟件從本質上是在模擬現實業務運行的過程,是由一個個交互活動組成的,因此,在分析階段需要梳理出業務的活動是怎樣的,通過圖形的方式記錄下來。

活動圖要體現出「參與者」、「活動起點」、「活動關鍵路徑」、「活動終點」,比如用戶下單,有「瀏覽」、「加購」、「支付」等活動。通過活動圖可以看出業務的生命週期是怎樣的,能夠抓住業務的關鍵流程。

1.2.2 用例圖

用例圖是強調用戶的目標和觀點,是文本化的情節描述,用例從本質上講並不是圖,它是文本,用圖形是簡化了表達形式,它核心有三點:「參與者」、「要做什麼」、「結果是怎樣的」。用例圖是對活動圖的細化,對其中的一個活動定義出要實現怎樣的目標。場景又是對用例圖的細化,你會發現,從目標到實現,一步一步地細化下來,細化是對擴大對認識的理解,不在認識範圍內,也就不會去做。

1.2.3 順序圖

將場景通過順序圖表達出來,它核心強調的是系統應該提供怎樣的能力,注意順序圖與時序圖的區別,順序圖是人與系統的交互,它想表達的是系統應該提供怎樣的能力滿足用戶的訴求,而時序圖系統內部實現,強調的是如何實現這種能力。

其實到順序圖這一步,基本上系統要提供的能力就清楚了,當然,這裏是知道系統要做什麼,至於要怎麼實現它並不關心。以上我認爲它是分析階段,把用戶想實現的內容清楚地定義出來,接下來就是思考怎麼實現。

1.2.4 時序圖

時序圖有兩種作用:一是表達功能是如何實現的;另一個是看責任分配是否合理。第一點比較好理解,一個功能實現是由多個不同的對象組合來實現,對象間有交互依賴。第二點是評判對象設計是否合理,如何兩個對象頻繁交互,是不是可以合併在一起,如何一對象中的操作過多,是不是可以拆解。

1.2.5 類圖

類圖的作用也有兩種:一是表達屬性和職責;另一個是層次結構。類中的屬性和職責是一個統一體,屬性體現的是認知能力,職責體現的是行爲能力,擁有怎樣的認識,就會產生怎樣的行爲。類不是一個孤零零的個體,它與其它的類之間有依賴、協作關係,因此,類圖中體現繼承、依賴、泛化、包含等關係。

1.3 原則篇

軟件設計原則汗牛充棟,簡化下來就三點:「複用」、「變化」、「認知複雜度」,好的設計處處體現設計原則,把這些原則刻畫到骨子裏,而不是刻意體現,如同「沒有規矩不成方圓」一樣,重點是要理解爲什麼要這些原則,從本質上講是爲了軟件能夠「多快好省」地完成。

利潤 = 收入 - 成本,從這個公式中,很明顯我們想要實現利潤最大化,怎麼辦呢,有兩個方法:一是收入變多,最好地方式是實現規模化;二是成本降低,不需要或者很少投入成本。從這兩點中,引申出「複用」和「變化」兩個原則,複用是不投入或者少投入實現功能,相比從頭做是不是要節省成本呢,我們的產品不可能一成不變的,那麼變化是在所難免的,如果能支撐靈活地擴展是不是也能節省成本呢。

1.3.1 複用

從上面的分析看,「複用性」的重要程度不言而喻,比起煙囪式開發,複用的成本要低得多,所以產生出了xx平臺、xx中臺,它們的本質目的還是爲了複用,減少重複開發成本。實現複用的手段有很多,複用的程度也不一樣,這個就要靠平時的積累,就像醫生積累「藥」和「藥方」一樣,這些都是我們解決問題的「工具」。先要有複用的思維,否則只有工具也是無用的,不知道要怎麼用、在哪裏用。

不同的場景,複用採用的設計方法是不一樣的,舉幾個例子方便大家理解。

  • 完全複用

最簡單的複用是100%的複用,比如加法計算操作,它肯定是100%複用的,只用傳輸不同的數字進去,就可以計算出結果。一般完全複用的是工具型的能力,它與具體的業務語義無關。

  • 配置化複用

這一類的複用程度接近100%,只用配置一些與業務相關的具體的參數即可,比如對賬場景,配置兩個不同的數據源表,再配置對賬規則即可完成對賬。這一類場景適用於業務比較固定,流程是通用的,並且差異變化是可枚舉的。

  • 部分複用

然而,在實現世界中,沒有太多像完全複用的事情,如果變化還不能通過配置化來實現,可以使用「模板方法」或「策略模式」,將變化延遲到子類中去實現,這種方法在大家日常工作很常見,也有的使用SPI擴展點實現。這一類複用場景是有明確的「主流程」,只有少量的變化隨業務變化,變化也是可枚舉的,那麼就可以抽象出擴展點。

還有一類變化是很難枚舉的,不知道會有怎樣的變化,此時最好的方法是通過「事件」解耦,主流程完成之後,發一個事件消息出來,誰關注就去實現該功能,本質上講Spring中Aware機制也屬於事件的處理方法。

  • 完全不能複用

還有一類完全不能複用,但要抽象出標準的接口,比如常用的數據庫操作,Connection、Statement就是標準的接口,不同的數據庫廠商實現具體的數據庫操作。不要覺得這種沒有意義,它從更高的層面定義了規範,使用者是面向抽象使用,可以不關注具體的實現是怎樣的。

1.3.2 變化

複用和變化是一起出來的,軟件唯一不變的是變化,怎麼支撐未來更好地擴展是我們要思考的,如果一個功能千年不變,怎麼簡單就怎麼實現,而如果有變化的話,那就需要好好地設計,用最少的成本去支撐未來的變化。比較難的是要洞察出什麼在變化,這個還真不是那麼好想到的,需要有行業經驗積累,看多了、實踐多了,會發現裏面的一些門道。

舉一個應對變化的例子,稅務在計稅時,不同的業務計稅規則不一樣,有的金本位要計稅,有的不需要計稅,有的非金本位要計稅,有的不需要。如果放在一個大的擴展點中實現,那麼這就是典型的面向過程的設計思維,我們抽象出了「計稅表達式」這個實體來應對變化,「計稅項」要不要計稅、計稅口徑是怎樣的,新業務接入通過「配置化」來解決。

1.3.3 認識複雜度

認識是分層次的,最高層越簡單,最低層越複雜,對於使用者來講,他希望看到的是簡單的內容,如果太複雜,很難上手,比如命令行式的操作系統和桌面式的操作系統,明顯桌面式的操作系統更受大衆的歡迎,這也是微軟在目前依然在操作系統市場佔用份額上還是大頭的原因。軟件分層的目的不僅是讓關注點分離,還有另外一個目的是降低認識複雜度,從簡單到複雜,這和我們軟件分析一樣的,從粗到細。

類的設計也是一樣的,舉一個例子,稅務在開發票時,開票這個模型結構需要調用者感知嗎,肯定不需要,因爲發票中有很多的領域概念,如開票主體、發票行等,用戶的目的就是開一張發票,他只用告訴你他知道的信息,不關心你內部要怎麼實現,基於這個思考,我們抽象出了「開票申請」這個實體出來,它本質是貼近業務場景的實體,所包含的信息也是有限的,極大地降低了認知複雜度。

二、系統分析與設計的2個案例

2.1 日誌框架

2.1.1 日誌框架分析

打印日誌在我們日常工作幾乎是人人都會接觸到,日誌的核心作用是記錄關鍵有效的信息,幫助我們快速地排查、定位問題,否則沒有日誌信息兩眼一抓瞎。根據我們的經驗,希望日誌中包含時間、類、方法、代碼行、關鍵日誌信息,用了這些信息就能方便我們排查問題。

根據上面的分析,我們很快可以畫出日誌的概念模型,如下圖所示。從本質上講,我們是將日誌信息存儲到指定的地方,如存儲文件中,輸出到控制檯上,另外還有日誌存儲的格式可以有多種,比如普通的格式,還有XML、HTML的格式等。

從概念模型上看,設計一個日誌框架並不複雜,但在設計階段中,還需要挖掘更多的信息,我們輸入的信息更多,設計時考慮的因素也就越全,更能滿足用戶的訴求。

2.1.1 日誌框架設計

2.1.1.1 貼近用戶的設計

站在用戶的視角,他關注的是信息以怎樣的格式存儲在哪裏,因此,有兩個概念用戶是要關注的,一個是「存儲目的地」,另一個是「存儲樣式」,這兩個是可以根據用戶的喜好配置的,將這兩個概念抽象下,「存儲目的地」抽象成「Appender」,「存儲樣式」抽象成「LayoutPattern」。除了配置外,用戶在使用時需要一個接口類,將其抽象成「Logger」門面類,只用簡單的調用日誌打印接口即可。

2.1.1.1 系統視角的設計

站在系統視角上,用戶在日誌打印時,他關注的是日誌信息,如時間、類名、方法名等信息他不會顯示去寫,因此還需要抽象一個概念出來表達日誌信息的概念,抽象成「LogRecord」,這樣概念類圖如下圖所示。

按照這個設計,很快可以設計出一個簡易的日誌框架,代碼結構如下所示。

Appender類如下所示,它定義的是一個模板方法,先調用LayoutPattern獲取格式化的日誌數據,然後再輸出到目標存儲上,Appender和LayoutPattern是可以獨立變化的,同時將寫操作延遲到子類中實現。

再細細想一下,日誌本身是爲了方便排查問題,但額外日誌的存儲是有性能開銷的,這個在設計時就要着重考慮了。如何減少寫日誌帶來的性能開銷呢,從三個方面考慮:

  • 大文件變小文件:打開一個1G的文件和打開1M的文件肯定是不一樣的,複雜問題的治理也是同樣的思路,拆分成小問題處理,因此在寫文件時可以按照「時間」、「容量大小」切分成小的文件。
  • 內存映射寫文件:傳統的IO寫數據,操作系統需要從用戶態切換到內核態,性能開銷會很大,可以使用內存映射的方式寫文件,提升IO性能。
  • 同步變異步寫文件:同步寫文件是需要等文件寫好了後再往下執行業務代碼,而異步就不一樣,它將日誌記錄存儲在一個隊列中,開戶另一個線程慢慢寫到文件中,不阻塞業務邏輯執行。

通過上面的分析,一個簡單的日誌框架隨着對它的理解加深,設計方案也在變化,核心是要能看到關鍵的技術問題有哪些,能提供哪些增量價值或差異化的價值。

2.2 定時任務框架

2.2.1 定時任務框架分析

定時任務在我們平時的工作中也經常使用,如每天定時發送郵件、定時做數據檢查等,完成定時任務的訴求也比較簡單,就是在指定的時間執行指定的任務,從這個角度看,業務模型並不複雜,如下圖所示。調度任務和調度時間是兩個獨立的變化維度。爲了讓任務能夠調度起來,還有一個「調度器」的概念並沒有在業務模型上體現出來,將要執行的任務,以及調度時間告知給調度器,調度器就能根據要求調度任務。

2.2.2 定時任務框架設計

2.2.2.1 貼近用戶的設計

站在用戶視角,他關心的有三點:

  • 任務要按照怎樣的規範去編寫,因此需要一個概念「Job」去表達,它定義了一個execute()接口。
  • 調度時間如何去表達,抽象出「Trigger」這個概念,表示到時就觸發任務執行。
  • 任務如何提交,需要一個門面類「Scheduler」去表達,接受用戶提交的任務。

2.2.2.2 系統視角的設計

站在系統的視角,很快能想到解決方法,用戶提交任務後要保存至一個隊列中「JobQueue」,「JobQueue」中存儲的是「JobDetail」,「JobQueue」包含了「Job」和「Trigger」兩部分信息,然後有一個調度線程「SchedulerThread」不斷掃描「JobQueue」,判斷當前任務是否要被執行,如果需要執行就調用「Job」的execute()方法,類的概念模型如下圖所示。

按照這個設計,很快可以設計出一個簡易的任務調度框架,代碼結構如下所示。

定時任務最關鍵的技術挑戰是查找要調度執行的任務,很容易想到對「JobQueue」中的「JobDetail」排序,發現Job的executeTime快到了就執行,然而排序是消耗CPU資源的,不同的排序算法時間複雜度也不一樣。怎麼降低排序算法的時間複雜度呢,最簡單的用最小堆排序算法,每次從堆頂獲取任務執行,而每次添加任務,又涉及到堆的調整,這個過程也是消耗CPU資源的。有沒有更快的算法呢,有人提出了時間輪的解決方法,類似鍾一樣,如一分鐘是60格,類似將任務的時間計算好,放到對應的格中,如果有多個相同的任務,就有一個鏈表鏈起來,xxl-job就用了時間輪的方法,將未來週期性需要 調度執行的任務放在時間輪中,時間輪的數據結構是一個Map。

這還是一個簡單的任務調度框架,還有很多問題沒有考慮到,比如任務分片、分佈式定時任務等,還是回到需求分析上,我們要做的功能邊界是什麼,目標是什麼,再去設計對應的解決方案。

三、總結

在日常技術方案設計時,最爲關鍵的是要能定義出關鍵的技術問題,有兩類問題是我們要着重考慮的:一是貼近用戶視角的便捷性設計,主要是對業務概念的抽象,用戶以最小的知識感知系統;另一個是系統的視角設計,除了完成功能外,還要定義出關鍵的技術問題以及度量的方法,如打印日誌帶來的系統開銷,怎麼做到對系統產生最小的影響;定時任務調度的調度算法設計,選用不同的數據結構的效果是不一樣的,普通的排序算法沒有堆排序算法好,但堆排序同樣涉及到性能開銷,這就讓我們不斷想更好地方案去解決。

作者:不拔

點擊立即免費試用雲產品 開啓雲上實踐之旅!

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章