《軟件方法(上)》讀書筆記

1、建模

1.1、業務建模之願景

重點1:通俗一點講,一個東西的願景就是:東西最應該賣個誰,對他有什麼好處?

重點2:願景是需求排序的主要依據。

重點3:老大、願景、需求都是基於現狀尋找最值得的改進。改進過後,又是新的現狀了,還是基於現狀尋找最值得的改進。進一步說也可以說,需求只有真假對錯,沒有變化。說需求有變化,那是從一個靜止時間點來看的。

1.1.1、願景




建模之願景

1.2、業務建模之業務用例圖

有了願景,我們知道老大對他所代表的組織的現狀的某些指標不滿意。接下來就可以研究組織,弄清楚到底是組織的哪些環節造成了這些指標比較差,這就是業務建模(Business modeling)的主要內容。

重點1:軟件系統只是組織的一個零件。組織裏面還有很多系統,其中最值錢的是千百年來一直在使用,現在依然是最複雜的系統——人腦系統。

重點2:開發團隊發現需求“容易變化”。根源之一是需求的來路不正,沒有把系統當作一個零件放在組織中來看,靠拍腦袋得出需求,導致得到的系統需求是錯的。

1.2.1、業務角色

① 業務執行者

以某組織為研究對象,在組織之外和組織交互的其他組織(人群或機構)就是該組織的執行者。以一家商業銀行為研究對象,觀察在它邊界之外和它打交道的人群或機構,可以看到儲戶來存錢,企業來貸款,人民銀行要它作監督等等,這些就是該商業銀行的執行者,如下圖所示:




業務執行者(一)

這裏要注意的是,作為觀察者的建模人員本身是一個人腦系統,所以在觀察組織邊界時,直覺上觀察到的不是組織之間的交互,而是組織派出的系統之間的交互,但是一定要把它理解成組織間的交互,因為談論業務執行者時,研究對象是組織,所以外部對應物——業務執行者也應該是組織。例如:以某國稅局為研究對象,可以觀察到企業財務人員到國稅局報稅,但業務執行者不是企業財務人員,而是企業。也許到後來,企業財務人員和國稅系統交互,又或許再後來是企業系統與國稅系統交互,從組織的抽象級別來看,都應該理解為企業和國稅局這兩個機構之間的交互,如下圖所示:




業務執行者(二)

② 業務工人

組織內的人稱為業務工人,例如某商業銀行裏面的營業員。業務工人是可以被替換的人腦零件,它可能被其他業務工人替換,但更有可能被業務實體替換。

③ 業務實體

業務實體是組織中的非人智能系統,例如銀行的ATM、點鈔機、營業系統。

1.2.2、識別業務用例

重點1:業務用例指業務執行者希望通過和所研究組織交互獲得的價值。業務用例是組織的價值,不會因為某個人腦系統或電腦系統的存在或消失而改變,好比如300年前的商業銀行和當前的銀行的業務用例是不變的,因為銀行提供價值的本質沒有改變。所以“這個系統的業務用例是什麼”這樣的說法是錯誤的。

重點1:用好用例,關鍵在於理解“價值”。價值是期望和承諾的平衡點、買賣的平衡點。例如以“醫院”為研究對象,真正的用例是“患者→看病”,而不是“患者→挂號”,患者→挂號”可以是以“挂號室”為研究對象的業務用例(如下圖)。所以做任何事情之前,要搞清楚“邊界”,沒有邊界會很容易盲目“拍腦袋”做一些努力但沒效果的事情。




業務用例識別

1.2.2.1、識別業務用例思路

識別業務用例的思路有兩條:

【從外到內】從業務執行者開始考慮,思考業務執行者和組織交互的目的(主要);

【從內到外】通過觀察組織的內部活動,一直問為什麼,向外推導出組織外部的某個業務執行者(補漏)。




業務用例識別思路

1.2.2.1.1、識別業務用例常犯錯誤

錯誤1 :把業務工人的行為當做業務用例。




錯誤用例識別(1)

錯誤2:業務用例隨待引入系統伸縮。




錯誤用例識別(2)

錯誤3:把害怕遺漏掉的擴展路徑片段提升為業務用例。




錯誤用例識別(3)

錯誤4:管理型業務用例。




錯誤用例識別(4)

總結:錯誤的根源主要源於:建模人員分不清問題和問題的解決方案。

1.3、業務建模之業務序列圖

1.3.1、描述業務流程的手段

本章節主要討論的是業務建模中最繁重的工作——描述業務用例的實現,即業務流程,然後改進它,推導出待引入系統的用例。目前描述業務流程的可選擇手段有文本、活動圖和序列圖,它們的主要區別如下(以財務部“員工→報銷”用例的實現為樣例):

● 文本




文本樣例

文本的缺點是不夠生動,所以在描述業務流程時很少使用文本方式。不過,描述系統用例(即系統需求)的流程時,文本是常用的,因為此時更注重精確,而且還要表達業務規則、性能等目前尚未被UML標準覆蓋的內容。

● 活動圖




活動圖樣例

序列圖是UML圖形描述業務流程的兩種選擇之一。活動圖的前身是流程圖,應該是在建模人員中使用頻率最高的圖形,是隨机械工程領域慢慢引入到計算機領域。不過,隨着編程語言表達能力越來越強,針對簡單的分支或循環邏輯畫圖在很多情況下已經變得沒有必要。

● 序列圖




序列圖樣例

序列圖與活動圖的比較如下:

1)活動圖只關注人,序列圖把人當作系統;

在上一章節中已經提到,現在的業務流程中已經有很多領域邏輯是封裝在業務實體而不是業務工人中,如果忽略非人智能系統,很多重要信息就丟掉了。

2)活動圖表示動作,序列圖強迫思考動作背後的目的;

序列圖可以更加清晰地表述業務工人或業務實體對外的責任,也就是用例的期望值。期望和承諾是用例和對象技術的關鍵思想,使用序列圖來做業務建模,“對象協作以完成用例”的思想就可以統一地慣竊業務建模和系統建模的始終。

3)活動圖“靈活”,序列圖“不靈活”;

不少人認為活動圖勝過序列圖的地方是它靈活,但這靈活是一把雙刃劍。活動圖很靈活,他的控制箭頭可以指向任何地方,就像編碼原始時代的“goto”語句,所以活動圖很容易畫。不過,“很容易畫”的活動圖也比較容易掩蓋建模人員對業務流程認識不足或者業務流程本身存在缺陷的事實。序列圖可通過alt、loop等結構化控製片段來描述業務流程,強迫建模人員用這種方式思考。

1.3.2、業務序列圖要點

1.3.2.1、消息代表責任分配而不是數據流動

序列圖中最重要的要點是消息的含義。A指向B的消息,代表“A請求B做某事”,或者“A調用B做某事的服務”,而“做某事”是B的一個責任。




序列圖消息含義

1.3.2.2、抽象級別是系統之間的協作

業務建模的研究對象是組織,出現在業務序列圖生命線上的對象,其最小顆粒是系統,包括人和非人系統,而系統之間最主要突出的是協作。所以“系統粒度”和“協作”是業務序列圖的關鍵要點,如果記住了這兩個關鍵點,就可以避免了對組織對象抽象的錯誤以及對協作理解的錯誤。




系統內部的組件暴露



表達了過細的交互步驟

以上說的兩種錯誤是把需求和分析的工作流的工作帶入了業務建模。第一樣例圖提到的系統內部的組件,應該在分析和設計工作流中描述;第二樣例圖提到了交互步驟,應該在需求工作流中描述。除了以上兩種抽象級別的錯誤,還有一種是:業務序列圖的內容和業務用例圖差不多,如下所示:




目標組織作為整體出現在業務序列圖中  

1.3.2.3、把時間看作特殊的業務實體

業務序列圖中,我們把時間看作特殊的業務實體。把時間看作上帝造好掛在天上的一個大鐘,向全世界各種系統發送時間消息,這樣,就和後面需要工作流中映射系統用例的時間執行者一致了,同時也幫助理清什麼情況下使用時間執行者的問題。




把時間當作一個系統

值得注意的一點是,時間和定時器不是一個概念,時間是“外系統”,定時器是其他系統用來和時間打交道的“邊界類”。世界上只有一個時間系統,但有無數的定時器。如果建模人員在識別系統用例時說“執行者是定時器”,那就錯了,執行者是時間。




時間和定時器的區別

1.3.2.4、為業務對象分配合適的責任

分配給業務對象的責任必須是該對象有能力承擔的,這需要我們自身必須對理解要十分清晰,畢竟我們自己說話有時候的會含糊。例如“工作人員用Word寫標書”這樣的說法好像可以接受,但如果按照說話的文字不假思索地隨便畫,會很容易導致對象責任分配不準確。




不恰當與不恰當的責任分配對比

1.3.3、現狀業務序列圖

業務序列圖描述的是業務流程,建模需要通過在現狀的業務序列圖基礎上找出改進的要點。如果要把現狀序列圖畫出來,就必須讓自己站在客觀的角度“親臨現場”,“如實”地把所看到的記錄下來,儘力描繪出真實的現狀。但說起來非常簡單,做到卻極其困難。總結到這裏,忽然讓我想起了彼得·德魯克。下面列出一些描述現狀時經常犯的錯誤。

1)錯誤:把想象中的改進當成現狀

很多時候造成這種錯誤,背後的原因很可能是根本沒有深入到組織流程中去做觀察和訪談,對現狀沒有認識,只好想像一個改進后的場景來應付。

2)錯誤:把“現狀”誤解為“純手工”

有的建模人員以為人做的事情才是本質,所以他畫的業務流程中,只有人,沒有非人系統,完全忽略了在技術進步下慢慢可以替代人的這些“業務實體”。

3)錯誤:把“現狀”誤解為“本開發團隊未參与之前”

開發團隊很容易會誤以為當他們開始參与組織流程完善而開發系統的時候當作“現狀”,這就是典型的“技術思維”,很多時候在開發團隊在參与組織流程完善前,組織已經經過了許多次“非系統級”的流程改進,這是站在組織角度去看問題的。

4)錯誤:把“現狀”誤解為“規範”

建模人員在建模業務流程時,照搬組織制定的規範,沒有去觀察實際工作中人們是如何做的,或者即使觀察到了人們實際沒有按照規範做,卻依然按照規範建模。這樣做,得到的業務流程是不真實的,畢竟上有政策,下有對策。

5)錯誤:“我是創新,沒有現狀”

互聯網創業公司的建模人員很容易犯的這個錯誤,動不動就說“我做的是互聯網創新,沒有現狀”,但他們已經忘記了歷史上所有的創新都是站在前輩這些巨人的肩膀之上這個事實。

6)錯誤:“我做產品,沒有現狀”

非定製系統的開發團隊進程拿這句話做接口。A公司的流程和B公司的流程有差異,中國的流程和外國的流程有差異,畫誰的現狀好的?問這個問題的時候,我想是開始開發團隊忘記了“做需求時把產品當項目做”的道理,在第2章節中也提到過“誰比誰更像”的重點。

1.3.4、改進業務序列圖

上面提到的現狀業務序列圖是對組織現狀的客觀描述,而改進業務序列圖是通過信息化手段去思考對業務現狀序列圖的一些改進。通常,信息化給人類的工作和生活帶來的改進有三種模式。

1.3.4.1、改進模式一:物流變成信息流

和信息的光電運輸比起來,用其他手段運輸的物的流轉速度就顯得太慢了,而且運輸成本會隨着距離的增加而明顯增加。如果同類物的不同實例之間可以相互取代,那麼可以提煉物中包含的部分或全部有價值的信息,在需要發生物流的地方,改為通過軟件系統交互信息,需要物的時候再將信息變成物,這樣就可以大大增加流轉速度和降低流轉成本。




物流變成信息流改進

1.3.4.2、改進模式二:改善信息流轉

軟件系統越來越多,而各個軟件系統之間溝通不暢,導致一個人為了達到某個目的可能需要和多個軟件系統打交道,如果把各軟件系統之間的協調工作改為一個軟件系統來完成,人只需要和單個軟件系統打交道,信息的流轉就改進了。




改善信息流轉

1.3.4.3、改進模式三:封裝領域邏輯

在業務流程中,有很多步驟是由人腦來判斷和計算的,領域邏輯封裝在人腦中。相對於計算機,人腦(業務人才)存在成本高,狀態不穩定、會徇私舞弊等問題。如果能夠提煉人腦中封裝的領域邏輯,改為封裝到軟件系統中,用軟件系統代替人腦,業務流程就得到了改進。換句話說,領域邏輯的封裝是對系統“內在”價值的提升,相對於前兩個改進模式有更高的要求和更大的困難。




封裝領域邏輯改進

1.3.4.4、改進思考方式:阿布爾思考法

在軟件開發團隊中,當有人提出新的想法時,經常會被馬上否定“太難了,做不了”,最終得到一個平庸的、毫無競爭力的系統。學會像阿布(俄羅斯大富豪羅曼·阿布拉莫維奇)一樣思考,有助於克服普通人因資源受限而不敢展開想象的思維障礙。阿布思考法分兩步:

1)假設有充足的資源去解決問題,得到一個完美的方案;

2)用手上現有的資源去山寨這個完美的方案。

其實,阿布思考法的核心思想就是,不要閉門造車,要“接地氣”的行動起來,主動去觀察,或主動尋找有用的信息去分析和調研,不要被各種局限被迫讓步。

2、需求

2.1、需求之系統用例圖

在“建模”階段我們研究和思考的對象是組織,從組織的整體性客觀地去發現組織如何可以通過信息化手段去優化流程。有了客觀的整體性分析和改進認知,接下來的“需求”階段需要深入到系統層面去思考了。按正常邏輯,每一步都有“承上啟下”的作用,本章節所研究的系統用例就是通過上一步業務序列圖中所映射出來的。

執行者和用例的概念在業務建模的學習中已經出現過,現在要研究的執行者和用例與業務建模時研究的執行者和用例相比,不同之處是研究對象,之前研究的是組織,現在研究的是系統。

2.1.1、系統執行者要點

系統執行者的定義:在所研究系統外,與該系統發生功能性交互的其他系統。

2.1.1.1、系統是能獨立對外提供服務的整體

封裝了自身的數據和行為,能獨立對外提供服務的東西才能稱為系統。不了解這點,建模人員很容易把“添加一些功能”當作“研發新系統”。




劃分系統用例

2.1.1.2、系統邊界是責任的邊界

系統執行者不是所研究系統的一部分,是該系統邊界外的另一個系統。這裏的系統邊界不是物理的邊界,而是責任的邊界。




錯誤的遙控軟件用例圖    



正確的遙控軟件用例圖

 

2.1.1.3、系統執行者和系統有交互

外系統必須和系統有交互,否則不能算是系統的執行者。如一名旅客來到火車站售票窗口,告訴售票員目的地和車次,售票員使用售票系統幫助旅客購買火車票,這個場景中,和火車票系統交互的是售票員,他是售票系統的執行者。




旅客不是售票系統的執行者

如果火車售票系統現在已經提供了旅客自行購票的接口,例如互聯網購票、售票機等,這種情況下,旅客也是售票系統的執行者。不過“售票員→售票”和“旅客→購票”是兩個不同的用例。

2.1.1.4、交互是功能性交互

上面說的交互還引出一個問題:假設售票員使用鼠標和售票系統交互,按道理,比起銷售員,鼠標里售票系統更近,為什麼不把鼠標作為售票系統的執行者呢?還有,假設售票系統運行在Windows操作系統之上,那麼Windows是不是售票系統的執行者?其實吧,辨別這些問題的要點就是:執行者和系統發生的交互是系統的功能需求。鼠標和操作系統跟售票系統的交互都不是售票系統的核心域概念。售票員和售票系統之間的交互才是,所以售票員才是售票系統的執行者。




售票系統的功能需求

2.1.1.5、系統執行者可以是人或非人系統

系統執行者可以是一個人腦系統,也可以是一個非人智能系統,甚至是一個特別的系統——時間。在軟件業的早期,一個系統的執行者往往全部都是人。隨着時間的推移,系統的執行者中非人執行者所佔的比例越來越多。用例的優勢在於“執行者”和“涉眾”的概念,把演員和觀眾分開。演員(執行者)在台上表演,觀眾(涉眾)在台下看,演員表演什麼是由觀眾的口味決定的,演員可以不是人,但觀眾肯定是人。演員如果是人類,那麼在觀眾席上也會有一個位置,不過在第幾排就不知道了(權重等級)。用例使用“執行者”和“涉眾”代替了原來的“用戶”是一個非常大的突破,建模人員如果過多地關注“用戶”,混淆了真正真正重要的前排“涉眾”的需求,把操作人員當前重要的調研對象(非關鍵人員),那麼花在重要的前排涉眾(關鍵人)身上的時間可能就不夠了。越來越多的系統執行者不是人類,也就是說沒有“用戶”。




從“執行者都是人”到“執行者有一些是人”

 

2.1.1、識別系統執行者

2.1.2.1、從業務序列圖映射系統執行者

如果沒有做業務建模,識別系統執行者只能靠頭腦風暴。例如:什麼人會使用系統來工作?什麼人負責維護系統?系統需要和哪些其他智能系統交互?有沒有定時引發的事件?等等問題。有了業務建模,可以直接從業務序列圖映射即可。業務序列圖上,和所研究系統有實線相連的對象映射為所研究系統的執行者。




業務序列圖:需找租客線索  



從業務序列圖映射得到系統執行者

 

2.1.3、系統用例要點

2.1.3.1、價值的買賣的平衡點

系統用例的定義:系統能夠為執行者提供的、涉眾可以接受的價值。和業務用例相比,研究對象從組織變成了系統,要理解好系統用例,重點依然是之前所強調的買賣平衡點、期望和承諾平衡點。

用例之前的許多需求方法學,把需求定義為思考系統“做什麼”,用例把需求提升到思考系統“賣什麼”的高度。這種思考是非常艱難的,因為它沒有標準答案,只有最佳答案。要得到這個答案,不能靠拍腦袋,必須揣摩涉眾。要得到合適的用例,需要有一顆善於提擦他人的心。




ATM和程序員人腦系統的用例

2.1.3.2、價值不等於可以這樣做

有些人會較真,還是以ATM為例子,有些人會因為“難道ATM放在那裡我就不能登錄一下就離開嗎?我今晚下班就去ATM那裡登錄一下給你看,然後走人。”ATM確實能登錄,但登錄功能並非ATM的賣點,如果以一個“門禁系統”為研究對象,登錄就可以作為它的用例。

還有一種情況,例如科員可以有A和B用例,科長因為比科員的權力大,所以就能擁有科員的用例。用例的執行者只是表明這個用例是為這一類執行者而做的。但不代表系統一定要有權限控制以防止其他的人或電腦系統使用該用例。即時系統確實需要有權限控制,而且角色的劃分和執行者相近,也要把這兩者分開,更不可以因為系統不設權限控制,所以把執行者的名字合併為“用戶”。




用例劃分案例

有些書中會給出“最佳粒度原則”。例如:一個系統的用例最好控制在XXX個之內,一個用例的基本路徑最好控制在X步到X步之間……這些是沒有根據的。市場需要各種各樣的系統,有功能眾多的,也有功能單一的,也有交付複雜的,應該把屁股坐到涉眾那邊去,揣摩涉眾的心裏,實事求是地寫下來。如果建模人員在粒度問題上激烈爭吵以及糾纏不清,有可能已經犯了錯誤,最常犯的錯誤是把步驟當作用例。




錯誤:把步驟當作用例

2.1.3.3、增刪改查用例的根源是從設計映射需求

有一些用例圖,映入眼帘的用例是四個四個一組的,仔細一看,剛好對應看數據庫的四種操作。相當於把數據庫的各個表名加上新增、刪除、修改、查詢,就得到了用例的名字。有些建模人員確實也知道這個錯誤,但他們學乖了,乾脆把每四個用例合併,改名叫“管理XX”或(“XX管理”),然後新增、刪除、修改、查詢等用例再擴展它,可惜依然是換湯不換藥。




從數據庫視角得到的用例

 

2.1.3.4、從設計映射需求錯誤二:“復用”用例

增刪改查用例實際上就是從設計映射需求,導致“復用”用例的一種情況。在看看以下例子:




“復用”用例錯誤示例——缺陷管理系統

從不同的業務序列圖分別映射得到系統有右邊四個用例,但有的建模人員會動起心思:這些實現起來不都是針對“缺陷”表來“select X X X from缺陷表where X X X”嗎,合併成一個用例“查詢缺陷”多好!於是得到左邊的結果。實際上,右邊這四個用例面對的執行者不同,背後的涉眾利益也有差別。

當然,如果真的像這位建模人員講的,把“數據庫”,買回去就好,想怎麼折騰這信息都可以那不是更加簡單。其實,用例是涉眾願意“購買”的、對系統的一種“用法”,只要涉眾願意“購買”,當然越多越好。講到這裏,就要來說一個需求的基本要點:需求不考慮“復用”,如果考慮“復用”,要警惕自己是不是已經轉換到了設計視角來思考問題。

2.1.3.5、系統用例不存在層次問題

系統用例的研究對象就是某特定系統,不是組織,也不是系統內部的組件。如果存在“層次”上的疑惑,背後的原因是研究對象不知不覺改變了。

像醫院信息系統的用例,有人會畫成如下圖所示,原因可能是前面沒有畫業務用例圖和業務序列圖,所以建模人員頭腦里不知不覺把醫院信息系統的價值和醫院的價值混在一起了。




錯誤的“高層”用例:混淆組織的價值和系統的價值

還有以下的防汛系統用例圖,把系統的願景當成了“高層”用例:




錯誤的“高層”用例:把願景當作用例

以下更為常見的錯誤,為系統的“模塊”或“子系統”畫用例圖:




錯誤:模塊的用例  



用例仍然是系統的用例    



錯誤:子系統的用例

2.1.3.6、用例的命名是動賓結構

用例的命名是動賓結構,例如“取現金”。動詞前面可以加狀語,賓語前面可以加定語,把一句話的主語砍掉,剩下的可以用作用例的名字。

給用例起名時不要使用弱動詞。用例之前的需求技術,可能是以“名詞+動詞”的形式命名系統的功能,例如“發票作廢”,後來要改成用例的動賓結構了,有的建模人員就在前面加一個弱動詞“進行”,就變成了“進行發票作廢”,這個也是不合適的。

如果“名詞+動詞”已經成為行業中的一個術語,也未必要嚴格的動賓結構,例如“成果分析”是某行業的一個術語,也就不必硬要倒過來變成“分析成果”了。

2.1.4、識別系統用例

2.1.4.1、從業務序列圖映射系統用例

其實,只要認真做好業務建模,從業務序列圖上映射系統用例,得到的結果自然就會符合上面說的這些要點。

從業務序列圖中,從外部指向所研究系統的消息,可以映射為該系統的用例。現在我們繼續從“識別系統執行者”的用例中結合執行者和系統用例一起識別。




從業務序列圖上找到從外部指向所研究系統的信息    



從業務序列圖映射得到系統用例

 

在以上業務序列圖中,有一處消息是“外呼人員”指向“線索管理系統”的消息為“提供本人當天名單”,但在以上系統用例圖中,用例名改為了“查看本人當天名單”。因為序列圖上的消息代表“請求某系統做某事”,用例代表“用某系統來做某事”,一定要理解兩種圖的要點,所以有的地方需要調整。

在以上系統用例圖中,有的箭頭是從執行者指向用例,這樣的執行者稱為用例的主執行者,有的箭頭是從用例指向執行者,這樣的執行者稱為用例的輔執行者。主執行者主動發起用例的交互,輔執行者在交互的過程中被動參与進來。

值得注意一下,輔執行者這個概念是被誤用的比較多。最常見的錯誤是把信息的接收者或者將來可能使用信息的人當成輔執行者。另一種輔執行者的誤用剛好相反,把信息的來源當作輔執行者。




錯誤:把可能會用到所生產信息的人當作輔執行者  



錯誤:把提供用例所需要信息的人當作輔執行者

以上錯誤的原因很多是因為前面沒有畫業務序列圖,導致建模人員在畫系統用例圖的時候產生焦慮,總是希望在圖上多放一些信息,以免自己忘記了。一般來說,輔執行者是非人智能系統的情況較多,人腦系統作為輔執行者的情況比較少,所以碰到輔執行者是人的時候,要多留心。




正確:合適的輔執行者(因為辦卡需要用戶輸入密碼)

 

2.2、需求之系統用例規約

用例圖表達了用例的目標,但是對於完整的需求來說,這是遠遠不夠的。用例的背後封裝了不同級別的相關需求,我們需要通過書寫用例規約把這些需求表達出來。用例規約就是以用例為核心來組織需求內容的需求規約。用例規約的各項內容可以通過以下類圖來展示:




用例規約的內容

2.2.1、前置條件和後置條件

用例通過前置條件(precondition)、後置條件(postcondition)以契約的形式表達需求。用例相當於系統的一個承諾:在滿足前置條件時開始,按照裏面的路徑步驟走,系統就能達到後置條件。為了避免掉入“從實現角度看這樣可以那樣也可以”的陷阱,後置條件只需要寫出最想要的那個狀態即可。

● 前置條件:用例開始前,系統需要滿足的約束。

● 後置條件:用例成功結束后,系統需要滿足的約束。

2.2.1.1、前置條件、後置條件必須是系統能檢測的




系統必須能檢測前置、後置條件

以上圖為例,“錄入保單”用例的前置條件是錯誤的。業務代表是否已經把保單交給內勤,系統無法檢測,不能作為前置條件;同樣,“收銀”用例的後置條件也是不對的。顧客是否已經帶着貨物離開商店,系統也無法檢測,不能作為後置條件。

2.2.1.2、前置條件必須是用例開始前系統能檢測的




前置條件必須是用例開始前系統能檢測的

 以上圖所示,儲戶開始取現金的交互前,系統不知道儲戶是誰,要去多少錢,所以無法檢測“儲戶賬戶里有足夠的金額”這個條件。如果把前置條件設置為類似於“存在大於最低限額的現金”這樣的背景條件作為前提條件是可以的。就算很長時間沒人來ATM取現金,這個條件是否成立就擺在那裡。

2.2.1.3、前置後置條件是狀態,不是動作

例如,“經理→批假”的前置條件不能寫“員工提交請假單”,因為是一個動作不是狀態,應改為“存在待審批的請假單”。特別要注意的是,寫成“員工已經提交請假單”很可能也是不對的,因為狀態和導致達到某個狀態的行為不是一一對應的,請假單未必是員工自己提交的,也可以組長負責幫本組人員請假,也可能是從另外的系統批量導入。

如果分不清狀態和行為的區別,建模就會遇到很大的麻煩。後面的建模工作中,還會不斷討論狀態和行為的問題。

2.2.1.4、前置後置條件要用核心域詞彙描述

“系統正常運行”、“網絡連接正常”等放之四海而皆準的約束,和所研究系統沒有特定關係,不需要在前置條件中寫出來,否則會得到一堆沒有任何增值作用的廢話。

後置條件也不能簡單地把用例的名字加上“成功”二字變成“XXX成功”。例如“顧客→下單”的後置條件寫成“顧客已經成功下單”,這不是廢話嗎?更合適的後置條件是“訂單信息已保存”。

2.2.1.5、“已登錄”不應作為前置條件

“已登錄”是一個比較有爭議的情況,以購物網站為研究對象,登錄不是用例。這一點已經在前面的已經學習過,那如何處理登錄?

1)畫法一:把其他用例作為“登錄”的擴展




畫法一:把其他用例作為“登錄”的擴展

 

會員登錄后可以下單,也可以查看以往訂單,還可以退貨……所以上圖這個方法把下單、查看以往訂單畫出登錄的擴展。這是錯的。並不是先做A然後做B或C,B和C就成了A的擴展。

2)畫法二:把“登錄”作為被包含用例




畫法二:把“登錄”作為被包含用例

把“登錄”變成被其他用例包含(Include)的被包含用例(Include Use Case)。這樣做是正確的。登錄用例本來不存在,後來在寫用例規約的時候,發現“下單”、“查看以往訂單”等用例都有以下步驟:




“查看以往訂單”步驟

為了節省書寫用例規約的工作量,考慮把這些形成一個小目標的步驟集合(不是單個步驟)分離出來,作為一個被包含用例單獨編寫規約。這個用例只被其他用例包含,不由主執行者指向。所以,如果按照這個做法的話,“下單”用例規約的步驟里,應該有表示包含“登錄”用例的步驟集合:會員【登錄】。這裏的“登錄”二字加了粗括號表示這是一個被包含用例。它的步驟和約束在另外的地方描述。當然,不喜歡用粗括號可以用下劃線等其他方法以示區分。

3)畫法三:其他用例以“已登錄”作為前置條件




畫法三:其他用例以“已登錄”作為前置條件

有些人覺得畫法二會讓好些用例會出現會員【登錄】,看起來有些礙眼,就想能不能把它提到前置條件里,那就得到了畫法三。把“登錄”作為一個用例,“會員已經登錄”作為其他用例的前置條件。這樣用例的步驟看起來更清爽,但是嚴格來說這也是不對的,“登錄”不能作為購物網站的用例。

以上章節學習過,如果在做需求時考慮復用,可能已經陷入了設計的思維。能夠在多個用例中復用登錄的狀態,這是設計人員的本事,他甚至可以做到10個用例的界面都從一個模板生成,但不能因此就把這10個用例合併成一個。

2.2.2、涉眾利益

前提條件是起點,後置條件是終點,中間的路該怎麼走?這就要由涉眾決定了。也就是我們需要對關鍵人按重要程度排序(從前排到後排)去考慮他們的利益,根據這些利益去梳理正確的需求。以銀行ATM為例子,儲戶在ATM取現金的時,涉及的涉眾利益如如下:

● 儲戶:希望方便;擔心權益受損。

● 銀行負責人:希望安全;希望節約運營成本。

正是這些涉眾利益的交鋒之下,目前我們日常生活中所看到的ATM的用例片段如下:




ATM用例片段

從步驟1有設計約束“通過磁條卡或芯片卡提交賬戶號碼”看,這是為了照顧儲戶“方便”的利益。在銀行角度,雖然儲戶是上帝,為了儲戶更加方便,不用密碼更方便的。但從銀行角度要考慮安全問題,不可能不設置密碼,但為什麼只設置6位而不是8位或者更多呢?這又是“安全”和“方便”交鋒后的妥協……

2.2.2.1、涉眾的來源

1)涉眾來源一:人類執行者

用例的執行者如果是人類,當然是用例的涉眾。執行者如果不是人類,就不是涉眾,因為它沒有利益主張。




考慮人類執行者之後的涉眾

上圖保險系統的“內勤→錄入保單”用例中,內勤是人類,是涉眾,而OA系統不是人類,不是涉眾。

2)涉眾來源二:上游

執行者要使用系統做某個用例,可能會需要一些資源,這些資源的提供者可能就是用例的涉眾。還是以“內勤→錄入保單”為例,保單由業務人員代表提供給內勤。如果內勤喝醉了酒亂錄,信息錯得一塌糊塗,業務代表的利益就被損害了。所以,考慮到上游之後,“內勤→錄入保單”用例的涉眾有內勤和業務代表了。




考慮上游之後的涉眾

3)涉眾來源三:下游

執行者使用系統做某個用例,產生的後果會影響到其他人。這些受影響的人也是涉眾。還是以“內勤→錄入保單”為例,如果系統做得不好,沒有檢測內勤錄保單時是否填了必填項就放了過去,後面負責審核的經理工作量增加了。還有,OA系統雖然不是該用例的涉眾,但假如保險系統不停地向OA系統發送垃圾數據包,導致OA癱瘓,那麼OA系統維護人員的工作量就增加了。所以,OA系統維護人員也是下游的涉眾。考慮下游之後,“內勤→錄入保單”用例的涉眾如下圖所示:




考慮下游之後的涉眾

 

4)涉眾來源四:信息的主人

用例會用到一些信息,這些信息會涉及某些人,雖然這些人也許並不知道這個系統的存在,但他們是用例的涉眾。還是以“內勤→錄入保單”為例,保單的信息涉及被保人,投保人和受益人,如果信息出錯或泄露,這些人就會遭殃,所以他們是涉眾。因為這類涉眾可能和系統沒什麼接口,比較容易被忽略,所以要特別需要注意。




考慮信息的主人之後的涉眾

其實,前面的業務建模對識別涉眾起到了非常大的幫助,如果做需求前做了業務建模,會更加了解一件事情的前因後果,大多數涉眾都能夠從業務序列圖中看出來。如下圖所示:




業務建模可以幫助尋找涉眾

 

2.2.2.2、尋找涉眾利益

查理·芒格說過:說服別人要訴諸利益,而非訴諸理性。所以要學會思考涉眾的利益點是一門非常大的學問。尋找涉眾利益時,要“親兄弟,明算賬”,把不同涉眾各自關注的利益體現出來,而不是寫成一模一樣的。家裡兩夫妻對同一件事情都還有不同立場,更不用說一個組織裏面形形色色的涉眾了。例如,司機開車進廠裝化肥,工作人員通過地磅系統操縱地磅給車稱重。針對這件事,不同的涉眾可謂是“各懷鬼胎”:

● 化肥公司老闆——擔心公司內部人員貪污;

● 地磅操作員——希望操作簡便;擔心承擔責任;擔心繫統壞掉影響工作量;

● 倉管人員——擔心稱不準導致無謂的返工裝包;

● 買主——擔心進去時稱得輕了,出來時稱得重了,導致給少了化肥;

● 司機——擔心等候時間太長導致每天拉貨次數減少;

即使有些利益有時不方便白紙黑字寫出來共享,但至少建模人員要心知肚明,不能一團和氣了事。建模人員要仔細觀察和揣摩涉眾的痛苦,才能找到真正的涉眾利益,否則寫出來的“涉眾利益”往往很蒼白。例如以下例子:

● 護士——擔心出錯

這都是正確的無用廢話,誰都擔心出錯,但為什麼還是出錯?仔細調研過之後寫出來就生動多了:

● 護士——擔心自己的藥理學知識記錯,對藥物名稱相近的藥物計算錯劑量,導致給葯錯誤;

2.2.2.3、善於積累涉眾利益

需求是不斷變化的,我想這都是共識了,新系統肯定在功能或性能上和舊系統有所不同,否則還做什麼新系統呢。但是,背後的涉眾利益要穩定得多。看看之前ATM的例子出現的涉眾利益:

儲戶——希望方便;擔心權益受損;

銀行負責人——希望安全;希望節約運營成本;

其實這些涉眾利益不止適用於ATM,也適用於清朝的錢莊櫃檯、現在的銀行櫃檯、網上銀行和手機銀行。換句話說,越本質越穩定。

2.2.3、基本路徑

一個用例會有多個場景,其中有一個場景描述了最成功的情況,執行者和系統的交互非常順利,一路綠燈直抵用例的後置條件。這個場景稱為基本路徑。用例把基本路徑分離出來,目的是凸顯用例的核心價值。還是以ATM為例,發生在ATM上的場景有很多:

1)張三來了,插卡,輸入密碼,輸入金額,順順利利取到錢,高興地走了;

2李四來了,插卡,輸密碼,密碼錯,在輸,再錯,再輸,卡被吞掉了;

3王五來了,插卡,輸密碼,輸金額,今天取得太多不能取了……

以上三個場景,只有場景(1)是銀行在大街上擺放一台ATM的初衷。雖然場景(2)和(3)是難以避免,但場景(1)出現得越多越好,這是涉眾對ATM的期望。

書寫路徑步驟的時候需要注意以下一些要點。這些要點有重疊的地方,如果違反了其中一個要點,很可能會違反另外的要點。

2.2.3.1、按照交互四步曲書寫

執行者和系統按回合交互,直到達成目的。需要的回合數是不定的,可能一個回合足夠,也可能需要多個回合。一個回合中的步驟分為四類:請求、驗證、改變、回應。如下圖所示:




交互四步曲

在一個回合中,請求是必須的,同時還需要其他三類步驟中的至少一類。看看以下例子,可以看到,第一個回合只需要請求和響應,第二個回合則四類步驟都有。




回合制的交互示例

當時間作為主執行者而且不需要和其他輔執行者交互的用例中,可能會出現不需要回應的情況,而且只有一個回合,如下所示:




回合制的交互示例

在書寫步驟時要注意以下一些形式上的問題:

1)對於時間為主執行者的用例,回合中的請求步驟不寫“時間告知時間周期到了”,而是寫“當到達時間周期時”。

2)驗證步驟不寫“是否”。例如以上例子中,第4步寫“系統驗證註冊信息充分”,不寫“系統驗證註冊信息是否充分”,目的是要表達“充分”是基本路徑期望的驗證結果。

3)系統和輔執行者之間的交互可以看作是一種回應步驟,寫成“系統請求輔助執行者做某事”,例如“系統請求郵件列表系統群發郵件”。

2.2.3.2、只寫系統能感知和承諾的內容

看看以下例子:

……

4、系統反饋應收總金額

5、顧客付款

6、收銀員提交付款方式和金額

7、系統計算找零金額

8、系統反饋找零金額,打印收據

9、收銀員找零

……

顧客付款和收銀員找零是系統無法感知和承諾的。如果寫在步驟里,會讓人產生誤解:只要用了本系統,顧客就會乖乖付款,收銀員會乖乖找零——也許顧客忘記付款和收銀員忘記找零正式商場要解決的一個頭痛問題。

2.2.3.3、使用主動語句理清責任

把動作的責任人放在主語的位置,看看以下兩句話:

1)伊布從瓦倫西亞處得到傳球,舒梅切爾撲救伊布的射門。

2瓦倫西亞傳球,伊布射門,舒梅切爾撲救。

雖然上面一句比較文藝,但下面一句把責任理得更清晰。用例步驟也是如此:

系統從會員處獲得用戶名和密碼(錯)

會員提交用戶名和密碼(對)

用戶名和密碼被驗證(錯)

系統驗證用戶名和密碼(對)

會員要是不提交,就不要怪系統沒有動靜;會員要是提交了,系統不動彈,那就要怪系統了。做到規規矩矩說話,把責任理清楚,其實不容易。再列舉一些常見的“胡說八道”,如下所示:




規矩用詞

2.2.3.4、主語只能是主執行者或系統

寫需求,就是要把系統當作一個黑箱,描述它對外提供的功能以及功能附帶的質量需求。系統如何構造,不屬於需求描述的範圍,除非是涉眾強加的設計約束。所以步驟里不能出現“執行者請求前端系統做某事,前端系統請求後端系統做某事”“執行者請求客戶端做某事,客戶端請求服務端做某事”“執行者請求A子系統做某事,A子系統請求B子系統做某事”,就算這個系統最終的組成是分解成很多個部分,分佈在一百多個國家運行,需求里也只有兩個字:系統。前面已經學習過了,系統邊界是責任邊界,而非物理邊界,如下所示:




需求把系統看作是黑箱

 

2.2.3.5、使用核心域術語描述

路徑步驟應該使用核心域的術語來描述,也就是說,要說“人話”。以下以一個零件採購系統為研究對象,比較以下兩句話,哪一句是“人話”,哪一句是“鳥語”?

1)系統建立連接,打開連接,執行SQL語句,從“零件”表查詢……

2系統根據查詢條件搜索零件

其實,一眼就能看出,第一句是“鳥語”,第二句是“人話”了。不同職能多少會有主觀意識,做Java開發的覺得自己做的才是技術,需求屬於業務。但需求人員卻覺得自己做的也是技術,他們所研究的客戶才是業務。但客戶覺得自己做的才是技術……,這咋看起來有點像一個鄙視鏈。其實,大家做的都是“技術”,這是領域不同而已。應該用“核心域”和“非核心域”來代替“業務”和“技術”。

如果所研究系統是一個關係數據庫的腳本工具,核心域是關係數據庫領域,上面提到的“系統建立連接,打開連接,執行SQL語句”就成了合適的需求。

2.2.3.6、不要涉及界面細節

很多人寫需求的時候,會把界面的細節帶進來,例如:

● 會員從下拉框中選擇類別

● 會員從文本框中輸入查詢條件

● 會員單擊“確定”按鈕

這些界面細節很可能不是需求,更多是設計方案,背後可能隱藏更多的真正需求,也許是可用性需求“操作次數不超過5次”。但畢竟我們面對的客戶各種各樣的喜好都有,可能有些前排涉眾明確要求“一定要用下拉框”,那麼“下拉框”也是需求,但依然不能寫“會員從下拉框框中選擇類別”。因為這裏涉及兩類需求,她們的穩定性和變化趨勢不同,應該分開描述:

● 會員選擇類別(這是步驟)

● 通過下拉框來實現(這是設計約束)

用例的需求組織方式是分層的,從用例的路徑、步驟、約束,穩定性會越來越低,如下所示:




用例規約的需求層級

把需求分層級了,穩定和不穩定的需求就分開了,更有利於了解事情的核心本質。平時遇到的大部分“需求變更”發生在補充約束級別,例如輸入會員信息時加個微信字段(字段列表變了),調整結賬時的打折規則(業務規則變了)。級別越高的需求,內容越穩定。

很多時候做需求會把看得見和需求混為一談,真正的需求不見得是顯而易見的。需求判斷的標準不是涉眾是否看得見,而是涉眾是否在意。第一章已經學習過,需求和設計不是一一對應的,而是多對多的。

2.2.3.7、不要涉及交互細節

在步驟中,除了避免描述頁面細節,還要避免描述交互細節。例如有人會這樣寫:

● 會員每輸入賬戶名稱的一個字符

● 系統在界面中驗證當前輸入信息合法

寫的人有他的道理:系統不是等待提交后才驗證輸入信息是否合法,而是隨時驗證立即反饋,這樣使用戶體驗更好。其實,這隻是交互設計的一些技能。忍不住要在需求規約里描述界面和交互的細節,背後的原因和忍不住要思考內部代碼如何實現的原因是一樣的,都是對自己的設計技能沒有信心,害怕“現在想到了如果不忘記記下來以後就忘記了”。這些都是人性的弱點。

用例的步驟應該把焦點放在系統必須接收什麼輸入、系統必須輸出什麼信息以及系統必須做什麼處理這三個重點上,加上字段列表、業務規則、可用性需求等約束,足以表達各種需求。

關於用例的交互怎麼寫,是一個比較頭疼的問題。即使不涉及交互設計細節的問題,也免不了混進交互設計的成分,例如,為什麼要分兩個回合而不是一個回合?實際上涉眾更希望一個回合就能達到用例的目標。所以,建議觀點是在用例規約中把路徑步驟刪掉,只保留輸入輸出、涉眾利益和補充約束,交互的路徑步驟由交互設計人員決定。

2.2.3.8、需求是“不這樣不行”

許多需求人員之所以在需求崗位上,並不是因為他掌握了該掌握的需求技能,可能只是因為他工作年限足夠長該換到需求崗位了——和許多年齡到了就上崗的夫妻和父母相似。這樣的需求人員硬着頭皮做需求時,最常用的一招就是托着腦袋想“這個東西是什麼樣子”,然後畫一個界面原型拿去和涉眾確認。一旦涉眾說“差不多就這樣吧”,就把這個界面原型作為需求交給分析設計人員。在這一點上,互聯網公司的產品經理表現得尤為明顯。如果僥倖成功,就拚命鼓吹“原型大法好”,因為他只會這個。

當需求人員問問題時都是“這樣可以嗎”,相當於:

● 需求人員:界面這樣布局可以嗎?

● 涉    眾:(好用就行,我又不會做界面,問我可不可以我當然說可以了)可以。

● 需求人員:代碼這樣寫可以嗎?

● 涉    眾:(好用就行,我又不會寫代碼,問我可不可以我當然說可以了)可以。

如果問的問題改為“不這樣可以嗎?”,像下面這樣:

● 需求人員:界面不這樣布局可以嗎?

● 涉    眾:不可以,這是政府的規定,你們不要自己亂髮揮啊!

● 需求人員:代碼不這樣寫可以嗎?

● 涉    眾:不可以,這段代碼是我小舅子寫的,一定要這樣,否則不給錢。

這時,界面和代碼就成為了需求,當然,只是補充約束級別的需求。說到這裏,我們歸納出需求的判斷標準:需求是“不這樣不行”,而不是“這樣也行”。

2.2.4、擴展路徑

基本路徑上的每個步驟都有可能發生很多意外,其實某些意外是系統要負責處理的,處理意外的路徑就是擴展路徑。因為一個步驟上出現的意外及其處理可能有多種,所以同一步驟上的擴展路徑可能有多條。




擴展路徑和步驟

對於擴展路徑及其步驟的標號,本書採用的是Cockburn推薦的方法。擴展路徑的標號方法是在擴展步驟的数字序號後面加上字符序號,例如2a表示步驟2的第a條擴展路徑,2b表示步驟2的第b條擴展路徑。擴展路徑條件的最後加上冒號,接下來是該擴展路徑的步驟,標號方法是在擴展路徑編號後面加上数字序號,例如2a1。也就是說,步驟的編號以数字結尾,擴展路徑編號以字母結尾。如果多重擴展,那就繼續按此形式標註。還是以之前ATM“儲戶→取現金”用例為例。該用例規約加上擴展路徑之後如下所示:

基本路徑:

1. 儲戶提交賬戶號碼

2. 系統驗證賬戶號碼合法

3. 系統提示輸入密碼

4. 儲戶輸入密碼

5. 系統驗證密碼合法、正確

6. 系統提示輸入取現金額

7. 儲戶輸入取現金額

8. 系統驗證取現金額合法

9. 系統記錄取現信息,更新賬戶餘額

……

2a. 賬戶號碼不合法:

         2a1.系統反饋賬戶號碼不合法

         2a2.返回1

5a. 密碼不合法:

         5a1.返回3

5b. 密碼合法但不正確:

         5b1.系統驗證當日取款累計輸錯密碼次數不超過3

                   5b1a.當日取款累計輸錯密碼次數超過3次:

                            5b1a1.系統關閉賬戶

                            5b1a2.用例結束

         5b2.系統反饋密碼不正確

         5b3.返回3

8a. 取現金額不合法:

         8a1.返回6

有一點需要注意,和輔執行者交互的步驟很有可能會出現擴展。例如:

4. 系統請求短信平台發布信息

……

擴展

……

4a. 短信平台無響應:

         4a1.系統反饋短信平台無響應

……

但,如果系統不需要從外部系統那裡得到任何結果,這個外系統就不是輔執行者,所以它出現故障會不會導致擴展的討論是沒有意義的。例如:

5. 系統向經理的电子郵箱通知有新的待審批申請

除了以上步驟之外,從其他步驟產生擴展路徑一定要非常謹慎,否則容易讓不屬於需求的內容混進用例規約中。特別要注意下面幾點。

1)能感知和要處理的意外才是擴展

2)設計技能不足導致的錯誤不是擴展

3)不引起交互行為變化的選擇不是擴展

4)界面跳轉不是擴展

2.2.5、補充約束

路徑步驟里描述的需求是不完整的。例如:

用例名:發布講座消息

……

1. 工作人員輸入講座信息,請求發布

2. 系統驗證講座信息充分

3. 系統生成發布內容

4. 系統請求短信平台發布消息

5. 系統保存講座信息和發布情況

6. 系統反饋信息已經保存併發布

……

步驟1中的“講座信息”包括哪些內容?需要添加字段列表。步驟2中“充分”指什麼,需要添加業務規則。從步驟1到步驟6有沒有速度上的要求?需要質量需求。

如果補充約束的內容只和單個用例相關,可以直接放在該用例的規約中;如果補充約束適用於多個用例,可以單獨集中到另外的地方,從用例規約引用。

補充約束前面的編號不代表順序,而是表示該約束綁定的步驟編號,如果某條補充約束不是針對某一步驟,而是針對多個步驟甚至整個用例,前面的編號可以是“*”,如下所示:

5. 發布情況=發布時間+工作人員

補充約束的類型可用類圖表示:




用例的補充約束

2.2.5.1、字段列表

字段列表用來描述步驟里某個領域概念的細節。例如上面“發布講座消息”用例的步驟中,步驟1、3、5都需要分別添加字段列表。

字段列表可以用自然語言表達,例如:

字段列表

1. 講座信息包括:舉辦時間、地點、專家信息、主題、簡介。專家信息包括:姓名、單位、頭銜。

也可以用符號表達,例如:

字段列表

1. 講座信息=舉辦時間+地點+專家+主題+簡介

1. 專家=姓名+單位+頭銜

表示的符號可以採用過去數據字典常用的符號。例如:“+”表示數據序列,“()”表示可選項,“{}”表示多個,“[|||]”表示可能的取值。例如:

註冊信息=公司名+聯繫人+電話+{聯繫地址}

聯繫地=州+城市+街道+(郵編)

保存信息=註冊信息+註冊時間

客房狀態=[空閑|已預定|佔用|維修中]

很多時候,做需求會把設計的細節帶上,例如“電話號碼varchar(255)”或把數據模型圖附上,這是不準確的。數據模型是設計,設計應該來源於需求,而非空想一個設計,然後把他當成需求。

2.2.5.2、業務規則

業務規則描述步驟中系統運算的一些規則,例如上面“發布講座消息”用例的步驟2中的“充分”沒有說清楚,需要添加業務規則,例如:

業務規則:

2. 必須有的項包括:時間、地點、專家、主題

其實,主要涉眾能理解,行業上適用的任何方式(例如數學、物理公式)都可以用來表達業務規則。描述業務規則時要注意:業務規則不等於實現算法。因為業務規則也是一種需求,也是從涉眾的角度看“不這樣不行”的。例如,研究一款為盲人或殘疾人而做的語音輸入軟件,用例規約有如下片段:

3. 系統將語音輸入翻譯為文字

業務規則

3. 採用XX識別算法

這樣的業務規則可能是有問題的,如果前排是盲人或殘疾人,他們是不懂什麼叫“XX算法”,也不在意你是否用了這個算法,他們在意的需求是:“背景噪音強度為XX的情況下,識別率應該在XX以上”。當然也不排除前排涉眾是有意推廣某廠家的識別技術,那麼“採用XX識別算法”也可以成為需求。

2.2.5.3、質量需求

系統滿足功能需求,說明系統能把事情做正確。在做正確的基礎上,系統還需要在做的過程中滿足一些指標,這些指標就是質量需求,也被稱為“非功能需求”。

1)可用性

可用性需求是對人類執行者和系統之間交互質量的度量。如果系統僅能正確達到用例目標,但交互太頻繁,人類執行者是不喜歡用的。在表達可用性需求時,僅僅說“系統容易使用”是不行的。這是無法量化的目標,合適的可用性需求應該是可以度量的,例如:




可用性度量

2)性能

性能包括速度、容量、能力等,例如:

① 系統應在0.5秒之內拍攝超速車的照片(速度)

② 應允許1000個執行者同時使用此用例(容量)

③ 在標準工作負荷下,系統的CPU佔用率應少於50%(能力)

在尋找質量需求時,性能類型的質量需求往往是最多的。

3)可靠性

可靠性表示系統的安全性和完整性,通常用平均無故障時間(MTBF,Mean Time Between Failures)和平均修復時間(MTTR, Mean Time To Repair)表示。可靠性需求往往不是針對單個用例,而是針對整個系統,可以在所有用例規約的最後,單獨用小篇幅描述。

4)可支持性

可支持性表示系統升級和修復的能力。例如:

① 95%的緊急錯誤應能在30個工作時內修復

② 在修復故障時,未修復的相關缺陷平均數應小於0.5

③ 升級新版本時,應保存所有系統設置和個人設置

和可靠性一樣,可支持性需求往往不是針對單個用例,而是針對整個系統,可以在所有用例規約的最後,單獨用小篇幅描述。

以上介紹了質量需求的種類,很多時候質量需求不用可以去尋找,按照前面說過的“不這樣行嗎”的標準,把混入需求的設計刪掉,然後問為什麼,背後往往隱含的就是質量需求,如下所示:




質量需求

2.2.5.4、設計約束

設計約束是在實現系統時必須要遵守的一些約束,包括界面樣式、報表格式、平台、語言等。

設計約束既不是功能需求,也不是質量需求。例如,“用Oracle數據庫保存數據”,其實用DB2也可以滿足同樣的功能需求和質量需求,但必須要用Oracle,因為客戶已經採購了許多Oracle數據庫軟件,如果不用,成本就會增加。

設計約束是需求的一種,也一樣要從涉眾的視角描述。在很多需求規約中,不少來自開發人員視角的設計偽裝成設計約束,混進了需求的隊伍。例如“系統應採用三層架構方式搭建”,涉眾並不了解“三層”好在哪裡,為什麼不是四層、五層等,然後問“為什麼”,背後的真正需求可能還是性能需求。

需求是問題,設計是解決方案,二者的穩定性不同。

 

2.3、需求啟發

2.3.1、需求啟發要點

需求人員要能夠像獵人一樣,用銳利的眼睛發現隱藏在叢林中的獵物,像偵探一樣,用縝密的思維判斷出偽裝成好人的兇手。但需求人員會有許多的障礙:如

1)需求的一個啟發障礙是知識的詛咒,意思是:一旦知道某個東西,就很難相像不知道它會是什麼樣子;

2)知識的詛咒在需求啟發中體現為溝通的困難;

3)需求啟發的另一個障礙是做和定義的不同;

但理解了以下兩個要點,有助於克服需求啟發中的障礙:

1)和涉眾交流的形式應該採用視圖,而不是模型;

2)和涉眾交流的內容應該聚焦涉眾利益,而不是需求;

2.3.2、需求啟發手段




需求啟發手段

2.3.3、需求人員的素質培養




需求人員的素質

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

PL真有意思(三):名字、作用域和約束

前言

這兩篇寫了詞法分析和語法分析,比較偏向實踐。這一篇來看一下語言設計里一個比較重要的部分:名字。在大部分語言里,名字就是標識符,如果從抽象層面來看名字就是對更低一級的內存之類的概念的一層抽象。但是名字還有其它相關的比如它的約束時間和生存周期等等

約束時間

約束就是兩個東西之間的一種關聯,例如一個名字和它所命名的事物,約束時間就是指創建約束的時間。有關的約束可以在許多不同的時間作出

  • 語言設計時
  • 語言實現時
  • 編寫程序時
  • 編譯時
  • 鏈接時
  • 裝入時
  • 運行時

這就是為什麼基於編譯的語言實現通常會比基於解釋器的語言的實現更高效的原因,因為基於編譯的語言在更早的時候就做了約束,比如對於全局變量在編譯時就已經確定了它在內存中的布局了

對象生存期和存儲管理

在名字和它們所引用的對象的約束之間有幾個關鍵事件

  • 對象的創建
  • 約束的創建
  • 對變量、子程序、類型等的引用,所有這些都使用了約束
  • 對可能暫時無法使用的約束進行失活或者重新約束
  • 約束的撤銷
  • 對象的撤銷

對象的生存期和存儲分配機制有關

  • 靜態對象被賦予一個絕對地址,這個地址在程序的整個執行過程中都保持不變
  • 棧對象按照後進先出的方式分配和釋放,通常與子程序的調用和退出同時進行
  • 堆對象可以在任意時刻分配或者釋放,它們要求更通用的存儲管理算法

靜態分配

全局變量是靜態對象最顯而易見的例子,還有構成程序的機器語言翻譯結果的那些指令,也可以看作是靜態分配對象。

還有像每次調用函數都會保持相同的值的局部變量也是靜態分配的。對於數值和字符串這些常量也是靜態分配。

還有用來支持運行時的各種程序,比如廢料收集和異常處理等等也可以看作是靜態分配

基於棧的分配

如果一種語言允許遞歸,那麼局部變量就不能使用靜態分配的方式了,因為在同一時刻,一個局部變量存在的實例個數是不確定的

所以一般對於子程序,都用棧來保存它相關的變量信息。在運行時,一個子程序的每個實例都在棧中有一個相應的棧幀,保存着它的參數、返回值、局部變量和一些簿記信息

基於堆的分配

堆是一塊存儲區域,其中的子存儲塊可以在任意時間分配與釋放。因為堆具有它的動態性,所以就需要對堆空間進行嚴格的管理。許多存儲管理算法都維護着堆中當前尚未使用的存儲塊的一個鏈接表,稱為自由表。

初始時這個表只有一個塊,就是整個堆,每當遇到分配請求時,算法就在表中查找一個大小適當的塊。所以當請求次數增多,就會出現碎片問題,也需要相應的解決

所以有廢料收集的語言其實就是對堆的管理

作用域作用

一個約束起作用的那一段程序正文區域,稱為這個約束的作用域。

現在大多數語言使用的都是靜態作用域,也就是在編譯時就確定了。也有少數語言使用動態作用域,它們的約束需要等到運行時的執行流才能確定

靜態作用域

在使用靜態作用域的語言,也叫作詞法作用域。一般當前的約束就是程序中包圍着一個給定點的最近的,其中有與該名字匹配的聲明的那個快中建立的那個約束。比如C語言在進入子程序時,如果局部變量和全局變量,那麼當前的約束就是與局部變量關聯,直到退齣子程序才撤銷這個約束

但是有的語言提供了一種可以提供約束的生存期的機制,比如Fortran的save和C的static

嵌套子程序

有許多語言允許一個子程序嵌套在另一個子程序的。這樣有關約束的定義通常來說都是首先用這個名字在當前、最內層的作用域中查找相應的聲明,如果找不到就直接到更外圍的作用域查找當前的約束,直到到達全局作用域,否則就發生一個錯誤

訪問非局部變量

上面提到的訪問外圍作用域的變量,但是當前子程序只能訪問到當前的棧幀,所以就需要一個調用幀鏈來讓當前的作用域訪問到外圍作用,通過調用順序形成一個靜態鏈

聲明的順序

關於約束還有一個問題,就是在同一作用域里,先聲明的名字是否能使用在此之後的聲明

在Pascal里有這樣兩條規則:

  1. 修改變量要求名字在使用之前就進行聲明
  2. 但是當前聲明的作用域是整個程序塊

所以在這兩個的相互作用下,會造成一個讓人吃驚的問題

const N = 10;

procedure foo;
const
  M = N; (*靜態語義錯誤*)
  N = 20;

但是在C、C++和Java等語言就不會出現這個問題,它們都規定標識符的作用域不是整個塊,而是從其聲明到塊結束的那一部分

並且C++和Java還進一步放寬了規則,免除了使用之前必須聲明的要求

模塊

恰當模塊化的代碼可以減少程序員的思維負擔,因為它最大限度的減少了理解系統的任意給定部分時所需的信息量。在設計良好的程序中,模塊之間的接口應盡可能的小,所有可能改變的設計決策都隱藏在某個模塊里。

模塊作為抽象

模塊可以將一組對象(如子程序、變量、類型)封裝起來。使得:

  1. 這些內部的對象相互可見
  2. 但是外部對象和內部對象,除非显示的導入,否則都是不可見的

模塊作為管理器

模塊使我們很容易的創建各種抽象,但是如果需要多個棧的實例,那麼就需要一個讓模塊成為一個類型的管理器。這種管理器組織方式一般都是要求在模塊中增加創建/初始化函數,並給每一個函數增加一個用於描述被操作的實例

模塊類型

對於像這種多實例的問題,除了管理器,在許多語言里的解決方法都是可以將模塊看作是類型。當模塊是類型的時候,就可以將當前的方法認為是屬於這個類型的,簡單來說就是調用方法變化了

push(A, x) -> A.push(x)

本質上的實現區別不大

面向對象

在更面向對象里的方法里,可以把類看作是一種擴充了一種繼承機制的模塊類型。繼承機制鼓勵其中所有操作都被看作是從屬於對象的,並且新的對象可以從現有對象繼承大部分的操作,而不需要為這些操作重寫代碼。

類的概念最早應該是起源於Simula-67,像後來的C++,Java和C#中的類的思想也都起源於它。類也是像Python和Ruby這些腳本語言的核心概念

從模塊到模塊類型再到類都是有其思想基礎,但是最初都是為了更好的數據抽象。但是即使有了類也不能完全取代模塊,所以許多語言都提供了面向對象和模塊的機制

動態作用域

在使用動態作用域的語言中,名字與對象間的約束依賴於運行時的控制流,特別是依賴子程序的調用順序

n : integer

procedure first
  n := 1

procedure second
  n : integer
  first()

n := 2
if read_integer() > 0
  second()
else
  first()
write_integer()

這裏最後的輸出結果完全取決於read_integer讀入的数字的正負,如果為正,輸出就為2,否則就打印一個1

作用域的實現

為了跟蹤靜態作用域程序中的哥哥名字,編譯器需要依靠一個叫做符號表的數據結構。從本質上看,符號表就是一個記錄名字和它已知信息的映射關係的字典,但是由於作用域規則,所以還需要更強大的數據結構。像之前那個寫編譯器系列的符號表就是使用哈希表加上同一層作用域鏈表來實現的

而對於動態作用域來說就需要在運行時執行一些操作

作用域中名字的含義

別名

在基於指針的數據結構使用別名是很自然的情況,但是使用別名可能會導致編譯器難以優化或者造成像懸空引用的問題,所以需要謹慎使用

重載

在大多數語言中都或多或少的提供了重載機制,比如C語言中(+)可以被用在整數類型也可以用在浮點數類型,還有Java中的String類型也支持(+)運算髮

要在編譯器的符號表中處理重載問題,就需要安排查找程序根據當前的上下文環境返回一個有意義的符號

比如C++、Java和C#中的類方法重載都可以根據當前的參數類型和數量來判斷使用哪個符號

內部運算符的重載

C++、C#和Haskell都支持用戶定義的類型重載內部的算術運算符,在C++和C#的內部實現中通常是將A+B看作是operator+(A, B)的語法糖

多態性

對於名字,除了重載還有兩個重要的概念:強制和多態。這三個概念都用於在某些環境中將不同類型的參數傳給一個特定名字的子程序

強制是編譯器為了滿足外圍環境要求,自動將某類型轉換為另一類型的值的操作

所以在C中,定義一個計算整數或者浮點數兩個值中的最小值的函數

double min(double x, double y);

只要浮點數至少有整數那麼多有效二進制位,那麼結果就一定會是正確的。因為編譯器會對int類型強制轉換為double類型

這是強制提供的方法,但是多態性提供的是,它使同一個子程序可以不加轉換的接受多種類型的參數。要使這個概念有意義,那麼這多種類型肯定要具有共同的特性

顯式的參數多態性就叫做泛型,像Ada、C++、Clu、Java和C#都支持泛型機制,像剛才的例子就可以在Ada中用泛型來實現

generic
  type T is private;
  with function "<" (x, y : T) return Boolean;
function min(x, y : T) return T;

function min(x, y : T) return T is
begin
  if x < y then return x;
  else return y;
  end if;
end min

function string_min is new min(string, "<")
function date_min is new min(date, date_precedes);

像List和ML中就可以直接寫

(define min (lambda (a b) (if (< a b) a b)))

其中有關類型的任何細節都由解釋器處理

引用環境的約束

提到引用環境的約束就有兩種方式:淺約束和深約束

推遲到調用時建立約束的方式淺約束。一般動態作用域的語言默認是淺約束,當然動態作用域和深約束也是可以組合到一起的。
執行時依然使用傳遞時的引用環境,而非執行時的引用環境。那麼這種規則稱為深約束,一般靜態作用域的語言默認是深約束

閉包

為了實現神約束,需要創建引用環境的一種顯示錶示形式,並將它與對有關子程序的引用捆綁在一起,這樣的捆綁叫做閉包

總而言之,如果子程序可以被當作參數傳遞,那麼它的引用環境一樣也會被傳遞過去

一級值和非受限生存期

一般而言,在語言中,如果一個值可以賦值給變量、可以當作參數傳遞、可以從子程序返回,那麼它被稱為具有一級狀態(和我們在js中說函數是一等公民一個含義)。大多數的語言中數據對象都是一級狀態。二級狀態是只能當作參數傳遞;三級值則是連參數也不能做,比如C#中一些+-*/等符號。

在一級子程序會出現一個複雜性,就是它的生存期可能持續到這個子程序的作用域的執行期外。為了避免這一問題,大部分函數式語言都表示局部變量具有非受限的生命周期,它們的生命周期無限延長,直到GC能證明這些對象再也不使用了才會撤銷。那麼不撤銷帶來的問題就是這些子程序的存儲分配基於棧幀是不行了,只能是基於堆來分配管理。為了維持能基於棧的分配,有些語言會限制一級子程序的能力,比如C++,C#,都是不允許子程序嵌套,也就從根本上不會存在閉包帶來的懸空引用問題。

小結

這一篇從名字入手,介紹了名字與其背後的對象的約束關係、以及約束時間的概念;然後介紹了對象的分配策咯(靜態、棧、堆);緊接着討論了名字與對象之間建立的約束的生命周期,並由此引出了作用域的概念;進一步延伸出多個約束組成的引用環境的相關概念以及問題。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

Kafka冪等性原理及實現剖析

1.概述

最近和一些同學交流的時候反饋說,在面試Kafka時,被問到Kafka組件組成部分、API使用、Consumer和Producer原理及作用等問題都能詳細作答。但是,問到一個平時不注意的問題,就是Kafka的冪等性,被卡主了。那麼,今天筆者就為大家來剖析一下Kafka的冪等性原理及實現。

2.內容

2.1 Kafka為啥需要冪等性?

Producer在生產發送消息時,難免會重複發送消息。Producer進行retry時會產生重試機制,發生消息重複發送。而引入冪等性后,重複發送只會生成一條有效的消息。Kafka作為分佈式消息系統,它的使用場景常見與分佈式系統中,比如消息推送系統、業務平台系統(如物流平台、銀行結算平台等)。以銀行結算平台來說,業務方作為上游把數據上報到銀行結算平台,如果一份數據被計算、處理多次,那麼產生的影響會很嚴重。

2.2 影響Kafka冪等性的因素有哪些?

在使用Kafka時,需要確保Exactly-Once語義。分佈式系統中,一些不可控因素有很多,比如網絡、OOM、FullGC等。在Kafka Broker確認Ack時,出現網絡異常、FullGC、OOM等問題時導致Ack超時,Producer會進行重複發送。可能出現的情況如下:

 

 

2.3 Kafka的冪等性是如何實現的?

Kafka為了實現冪等性,它在底層設計架構中引入了ProducerID和SequenceNumber。那這兩個概念的用途是什麼呢?

  • ProducerID:在每個新的Producer初始化時,會被分配一個唯一的ProducerID,這個ProducerID對客戶端使用者是不可見的。
  • SequenceNumber:對於每個ProducerID,Producer發送數據的每個Topic和Partition都對應一個從0開始單調遞增的SequenceNumber值。

2.3.1 冪等性引入之前的問題?

Kafka在引入冪等性之前,Producer向Broker發送消息,然後Broker將消息追加到消息流中后給Producer返回Ack信號值。實現流程如下:

 

上圖的實現流程是一種理想狀態下的消息發送情況,但是實際情況中,會出現各種不確定的因素,比如在Producer在發送給Broker的時候出現網絡異常。比如以下這種異常情況的出現:

 

上圖這種情況,當Producer第一次發送消息給Broker時,Broker將消息(x2,y2)追加到了消息流中,但是在返回Ack信號給Producer時失敗了(比如網絡異常) 。此時,Producer端觸發重試機制,將消息(x2,y2)重新發送給Broker,Broker接收到消息后,再次將該消息追加到消息流中,然後成功返回Ack信號給Producer。這樣下來,消息流中就被重複追加了兩條相同的(x2,y2)的消息。

2.3.2 冪等性引入之後解決了什麼問題?

面對這樣的問題,Kafka引入了冪等性。那麼冪等性是如何解決這類重複發送消息的問題的呢?下面我們可以先來看看流程圖:

 

 同樣,這是一種理想狀態下的發送流程。實際情況下,會有很多不確定的因素,比如Broker在發送Ack信號給Producer時出現網絡異常,導致發送失敗。異常情況如下圖所示:

 

 當Producer發送消息(x2,y2)給Broker時,Broker接收到消息並將其追加到消息流中。此時,Broker返回Ack信號給Producer時,發生異常導致Producer接收Ack信號失敗。對於Producer來說,會觸發重試機制,將消息(x2,y2)再次發送,但是,由於引入了冪等性,在每條消息中附帶了PID(ProducerID)和SequenceNumber。相同的PID和SequenceNumber發送給Broker,而之前Broker緩存過之前發送的相同的消息,那麼在消息流中的消息就只有一條(x2,y2),不會出現重複發送的情況。

2.3.3 ProducerID是如何生成的?

客戶端在生成Producer時,會實例化如下代碼:

// 實例化一個Producer對象
Producer<String, String> producer = new KafkaProducer<>(props);

在org.apache.kafka.clients.producer.internals.Sender類中,在run()中有一個maybeWaitForPid()方法,用來生成一個ProducerID,實現代碼如下:

 private void maybeWaitForPid() {
        if (transactionState == null)
            return;

        while (!transactionState.hasPid()) {
            try {
                Node node = awaitLeastLoadedNodeReady(requestTimeout);
                if (node != null) {
                    ClientResponse response = sendAndAwaitInitPidRequest(node);
                    if (response.hasResponse() && (response.responseBody() instanceof InitPidResponse)) {
                        InitPidResponse initPidResponse = (InitPidResponse) response.responseBody();
                        transactionState.setPidAndEpoch(initPidResponse.producerId(), initPidResponse.epoch());
                    } else {
                        log.error("Received an unexpected response type for an InitPidRequest from {}. " +
                                "We will back off and try again.", node);
                    }
                } else {
                    log.debug("Could not find an available broker to send InitPidRequest to. " +
                            "We will back off and try again.");
                }
            } catch (Exception e) {
                log.warn("Received an exception while trying to get a pid. Will back off and retry.", e);
            }
            log.trace("Retry InitPidRequest in {}ms.", retryBackoffMs);
            time.sleep(retryBackoffMs);
            metadata.requestUpdate();
        }
    }

3.事務

與冪等性有關的另外一個特性就是事務。Kafka中的事務與數據庫的事務類似,Kafka中的事務屬性是指一系列的Producer生產消息和消費消息提交Offsets的操作在一個事務中,即原子性操作。對應的結果是同時成功或者同時失敗。

這裏需要與數據庫中事務進行區別,操作數據庫中的事務指一系列的增刪查改,對Kafka來說,操作事務是指一系列的生產和消費等原子性操作。

3.1 Kafka引入事務的用途?

在事務屬性引入之前,先引入Producer的冪等性,它的作用為:

  • Producer多次發送消息可以封裝成一個原子性操作,即同時成功,或者同時失敗;
  • 消費者&生產者模式下,因為Consumer在Commit Offsets出現問題時,導致重複消費消息時,Producer重複生產消息。需要將這個模式下Consumer的Commit Offsets操作和Producer一系列生產消息的操作封裝成一個原子性操作。

產生的場景有:

比如,在Consumer中Commit Offsets時,當Consumer在消費完成時Commit的Offsets為100(假設最近一次Commit的Offsets為50),那麼執行觸發Balance時,其他Consumer就會重複消費消息(消費的Offsets介於50~100之間的消息)。

3.2 事務提供了哪些可使用的API?

Producer提供了五種事務方法,它們分別是:initTransactions()、beginTransaction()、sendOffsetsToTransaction()、commitTransaction()、abortTransaction(),代碼定義在org.apache.kafka.clients.producer.Producer<K,V>接口中,具體定義接口如下:

// 初始化事務,需要注意確保transation.id屬性被分配
void initTransactions();

// 開啟事務
void beginTransaction() throws ProducerFencedException;

// 為Consumer提供的在事務內Commit Offsets的操作
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
                              String consumerGroupId) throws ProducerFencedException;

// 提交事務
void commitTransaction() throws ProducerFencedException;

// 放棄事務,類似於回滾事務的操作
void abortTransaction() throws ProducerFencedException;

3.3 事務的實際應用場景有哪些?

在Kafka事務中,一個原子性操作,根據操作類型可以分為3種情況。情況如下:

  • 只有Producer生產消息,這種場景需要事務的介入;
  • 消費消息和生產消息並存,比如Consumer&Producer模式,這種場景是一般Kafka項目中比較常見的模式,需要事務介入;
  • 只有Consumer消費消息,這種操作在實際項目中意義不大,和手動Commit Offsets的結果一樣,而且這種場景不是事務的引入目的。

4.總結

Kafka的冪等性和事務是比較重要的特性,特別是在數據丟失和數據重複的問題上非常重要。Kafka引入冪等性,設計的原理也比較好理解。而事務與數據庫的事務特性類似,有數據庫使用的經驗對理解Kafka的事務也比較容易接受。

5.結束語

這篇博客就和大家分享到這裏,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或發送郵件給我,我會盡我所能為您解答,與君共勉!

另外,博主出書了《》和《》,喜歡的朋友或同學, 可以在公告欄那裡點擊購買鏈接購買博主的書進行學習,在此感謝大家的支持。關注下面公眾號,根據提示,可免費獲取書籍的教學視頻。 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

從零開始搭建前後端分離的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的項目框架之十二Swagger(參數)使用二

  引言

  在 中提到了 Swagger 的基本使用,僅限於沒有參數,沒有驗證的那種api文檔生成,那麼這篇就連接上篇繼續,在一般具有安全性、權限等驗證的接口上,

  都會在header/url中加上請求者的秘鑰、簽名等,當然也有可能添加到body等其它地方, Swashbuckle.AspNetCore 都支持這些寫法。

  如何使用 — 下面將介紹兩種使用方式

兩種方式參數設置到何處都是在  In屬性上,屬性對於值如下:    參考

  • query: 參数字段值對應放在url中
  • header: 參數值對應放在header param中
  • body: 參數對應放到請求體中
  • path: 參數應該對應放到請求路徑  // 具體貌似沒用
  • formData: 參數對應放到請求表單中

  第一種:將一個或多個參數保護API的“securityDefinitions”添加到生成的Swagger中。

這種是直接在文檔的右上方添加一個 Authorize 按鈕,設置了值后,每一個請求都會在設置的位置上加上相應的值,在 上一篇隨筆中的 ConfigureServices 方法中,

對應位置 services.AddSwaggerGen(options =>{}) 中的  XmlComments  下 添加代碼如下:

                options.AddSecurityDefinition("token", new ApiKeyScheme
                {
                    Description = "token format : {token}",//參數描述
                    Name = "token",//名字
                    In = "header",//對應位置
                    Type = "apiKey"//類型描述
                });
                options.AddSecurityDefinition("sid", new ApiKeyScheme
                {
                    Description = "sid format : {sid}",//參數描述
                    Name = "sid",//名字
                    In = "header",//對應位置
                    Type = "apiKey"//類型描述
                });
                //添加Jwt驗證設置 設置為全局的,不然在代碼中取不到
                options.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>> {
                    { "token", Enumerable.Empty<string>() },
                    { "sid", Enumerable.Empty<string>() },
                });

  添加完成后,運行起來看下效果,效果圖: 

 設置上對應值,調用測試方法,可以在header中取到剛設置到的值,

 這裡能看到,可以取到設置的參數了。這樣一來,在需要驗證的接口上,我們就可以通過接口文檔來測試了。基本不用再藉助postman等接口測試工具了。

但是,但是,這裡有一個問題,就是只要設置了參數值,每一次訪問都會在請求中帶上參數。

下面將介紹第二種方式,只給需要驗證用戶的接口上添加驗證參數。

  第二種:使用“filters”擴展Swagger生成器,來實現只在需要添加參數的方法上添加參數。複雜的可以根據自己的需求來添加對應參數

實現方式就是先新建一個類,名: SwaggerParameter ,實現 IOperationFilter 接口。SwaggerParameter 類代碼如下: 

    /// <summary>
    /// 自定義添加參數
    /// </summary>
    public class SwaggerParameter : IOperationFilter
    {
        /// <summary>
        /// 實現 Apply 方法
        /// </summary>
        /// <param name="operation"></param>
        /// <param name="context"></param>
        public void Apply(Operation operation, OperationFilterContext context)
        {
            if (operation.Parameters == null) operation.Parameters = new List<IParameter>();
            var attrs = context.ApiDescription.ActionDescriptor.AttributeRouteInfo;
            var t = typeof(BaseUserController);
            //先判斷是否是繼承用戶驗證類
            if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor descriptor && context.MethodInfo.DeclaringType?.IsSubclassOf(t) == true)
            {
                //再驗證是否允許匿名訪問
                var actionAttributes = descriptor.MethodInfo.GetCustomAttributes(inherit: true);
                bool isAnonymous = actionAttributes.Any(a => a is AllowAnonymousAttribute);
                // 需要驗證的方法添加
                if (!isAnonymous)
                {
                    operation.Parameters.Add(new NonBodyParameter()
                    {
                        Name = "sid",
                        In = "header", //query header body path formData
                        Type = "string",
                        Required = true,//是否必選
                        Description = "登錄返回的sid"
                    });
                    operation.Parameters.Add(new NonBodyParameter()
                    {
                        Name = "token",
                        In = "header", //query header body path formData
                        Type = "string",
                        Required = true,//是否必選
                        Description = "登錄返回的token"
                    });
                }
            }
        }
    }

 運行起來后,進入到  文檔頁面,可以看到右上角的 Authorize 按鈕已經不見了,在不需要驗證的方法上,也找不到相應需要設置參數的輸入框。就只有在需要驗證的接口上才有。

參考Swagger文檔圖如下: 

參考代碼圖如下:

 

效果圖: 

  這樣一來設置也就完成了。從上面就能看出,就只有需要用戶驗證的接口才會有相應參數。 

 

我的設置方式是先定義了用戶驗證控制器類,讓需要用戶驗證的控制器繼承該控制器,然後在該控制器中不需要用戶驗證的接口上加上 AllowAnonymous 屬性 

設置fitter時就可以根據上面提到的兩個點來進行判斷是否需要加上參數,如果不是這樣實現的,可以根據自己的需求變更fitter類,來控制文檔的生成。 

 

以上若有什麼不對或可以改進的地方,望各位指出或提出意見,一起探討學習~ 

有需要源碼的可通過此 鏈接拉取 覺得還可以的給個 start 和點個 下方的推薦哦~~謝謝!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

(三十五)golang–面向對象之多態

多態:變量具有多種形態,可以用統一的接口來調用不同的實現。

接口體現多態特徵:

(1)多態參數:之前所講的Usb接口案例,既可以接受手機變量,也可以接受相機變量,就體現了usb接口的多態;

(2)多態數組:

package main

import (
    "fmt"
)

type usb interface {
    start()
    stop()
}

type phone struct {
    name string
}

func (p phone) start() {
    fmt.Println(p.name, "手機開始工作")
}

func (p phone) stop() {
    fmt.Println(p.name, "手機停止工作")
}

type camera struct {
    name string
}

func (c camera) start() {
    fmt.Println(c.name, "相機開始工作")
}

func (c camera) stop() {
    fmt.Println(c.name, "相機停止工作")
}

type computer struct {
}

func (co computer) working(usb usb) {
    usb.start()
    usb.stop()
}

func main() {
    var usbArr [3]usb
    usbArr[0] = phone{"小米"}
    usbArr[1] = phone{"vivo"}
    usbArr[2] = camera{"尼康"}
    fmt.Println(usbArr)
    for i := 0; i < len(usbArr); i++ {
        usbArr[i].start()
        usbArr[i].stop()
    }
}

我們以前講到,數組是只能存儲同一種類型的數據,利用多態數組,就可以存儲不同的類型了;

如何將一個接口變量賦值給一個自定義類型的變量?使用類型斷言

類型斷言:由於接口是一般類型,不知道具體類型,如果要轉成具體類型,就需要使用類型斷言;要保持原來空接口指向的數據類型和斷言的數據類型一致;

為了避免輸出panic報錯,可以進行斷言判斷;

類型斷言實踐一:

我們給phone中加入一個方法call(),在調用usb變量時,usb.call(),肯定是不對的,因為usb可能是phone,也可能是camera,而camera是沒有這個函數的,因此,在調用的時候用類型斷言。

package main

import (
    "fmt"
)

type usb interface {
    start()
    stop()
}

type phone struct {
    name string
}

func (p phone) start() {
    fmt.Println(p.name, "手機開始工作")
}

func (p phone) call() {
    fmt.Println(p.name,"手機在打電話")
}

func (p phone) stop() {
    fmt.Println(p.name, "手機停止工作")
}

type camera struct {
    name string
}

func (c camera) start() {
    fmt.Println(c.name, "相機開始工作")
}

func (c camera) stop() {
    fmt.Println(c.name, "相機停止工作")
}

type computer struct {
}

func (co computer) working(usb usb) {
    usb.start()
    //如果usb還指向phone的結構體變量,則還需要調用call方法
    if phone, ok := usb.(phone); ok { phone.call() }
    usb.stop()
}

func main() {
    var usbArr [3]usb
    usbArr[0] = phone{"小米"}
    usbArr[1] = phone{"vivo"}
    usbArr[2] = camera{"尼康"}
    var com computer
    fmt.Println(usbArr)
    for i := 0; i < len(usbArr); i++ {
        com.working(usbArr[i])
    }
}

類型斷言實踐2:循環判斷輸入參數的類型

package main

import (
    "fmt"
)

type student struct {
    name string
}

func typeJudge(items ...interface{}) {
    for index, x := range items {
        switch x.(type) {
        case bool:
            fmt.Printf("第%v個參數是bool類型,值是%v\n", index, x)
        case int, int32, int64:
            fmt.Printf("第%v個參數是整數類型,值是%v\n", index, x)
        case float32:
            fmt.Printf("第%v個參數是float32類型,值是%v\n", index, x)
        case float64:
            fmt.Printf("第%v個參數是float64類型,值是%v\n", index, x)
        case string:
            fmt.Printf("第%v個參數是string類型,值是%v\n", index, x)
        case student:
            fmt.Printf("第%v個參數是student類型,值是%v\n", index, x)
        case *student:
            fmt.Printf("第%v個參數是*student類型,值是%v\n", index, x)
        default:
            fmt.Printf("第%v個參數類型不確定,值是%v\n", index, x)
        }
    }
}

func main() {
    var n1 float32 = 1.1
    var n2 float64 = 1.2
    var n3 int32 = 1
    var name string = "tom"
    var n5 bool = true

    stu1 := student{"jack"}
    stu2 := &student{"bob"}

    typeJudge(n1, n2, n3, name, n5, stu1, stu2)
}

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

【algo&ds】8.最小生成樹

1.最小生成樹介紹

什麼是最小生成樹?

最小生成樹(Minimum spanning tree,MST)是在一個給定的無向圖G(V,E)中求一棵樹T,使得這棵樹擁有圖G中的所有頂點,且所有邊都是來自圖G中的邊,並且滿足整棵樹的邊權值和最小。

2.prim算法

和Dijkstra算法很像!!請看如下Gif圖,prim算法的核心思想是對圖G(V,E)設置集合S,存放已被訪問的頂點,然後每次從集合V-S中選擇與集合S的最短距離最小的一個頂點(記為u),訪問並加入集合S。之後,令頂點u為中間點,優化所有從u能到達的頂點v與集合s之間的最短距離。這樣的操作執行n次,直到集合s中包含所有頂點。

不同的是,Dijkstra算法中的dist是從源點s到頂點w的最短路徑;而prim算法中的dist是從集合S到頂點w的最短路徑,以下是他們的偽碼描述對比,關於Dijkstra算法的詳細描述請

算法實現:

#include<iostream>
#include<vector>
#define INF 100000
#define MaxVertex 105
typedef int Vertex; 
int G[MaxVertex][MaxVertex];
int parent[MaxVertex];   // 並查集 
int dist[MaxVertex]; // 距離 
int Nv;    // 結點 
int Ne;    // 邊 
int sum;  // 權重和 
using namespace std; 
vector<Vertex> MST;  // 最小生成樹 

// 初始化圖信息 
void build(){
    Vertex v1,v2;
    int w;
    cin>>Nv>>Ne;
    for(int i=1;i<=Nv;i++){
        for(int j=1;j<=Nv;j++)
            G[i][j] = 0;  // 初始化圖 
        dist[i] = INF;   // 初始化距離
        parent[i] = -1;  // 初始化並查集 
    }
    // 初始化點
    for(int i=0;i<Ne;i++){
        cin>>v1>>v2>>w;
        G[v1][v2] = w;
        G[v2][v1] = w;
    }
}

// Prim算法前的初始化 
void IniPrim(Vertex s){
    dist[s] = 0;
    MST.push_back(s);
    for(Vertex i =1;i<=Nv;i++)
        if(G[s][i]){
            dist[i] = G[s][i];
            parent[i] = s;
        } 
}

// 查找未收錄中dist最小的點 
Vertex FindMin(){
    int min = INF;
    Vertex xb = -1;
    for(Vertex i=1;i<=Nv;i++)
        if(dist[i] && dist[i] < min){ 
            min = dist[i];
            xb = i;
        }
    return xb;
}

void output(){
    cout<<"被收錄順序:"<<endl; 
    for(Vertex i=1;i<=Nv;i++)
        cout<<MST[i]<<" ";
    cout<<"權重和為:"<<sum<<endl; 
    cout<<"該生成樹為:"<<endl; 
    for(Vertex i=1;i<=Nv;i++)
        cout<<parent[i]<<" ";
}

void Prim(Vertex s){
    IniPrim(s);
    while(1){
        Vertex v = FindMin();
        if(v == -1)
            break;
        sum += dist[v];
        dist[v] = 0;
        MST.push_back(v);
        for(Vertex w=1;w<=Nv;w++)
            if(G[v][w] && dist[w])
                if(G[v][w] < dist[w]){
                    dist[w] = G[v][w];
                    parent[w] = v;
                }
    }
}


int main(){
    build();
    Prim(1);
    output();
    return 0;
} 

關於prim算法的更加詳細講解請

3.kruskal算法

Kruskal算法也可以用來解決最小生成樹的問題,其算法思想很容易理解,典型的邊貪心,其算法思想為:

  • 在初始狀態時隱去圖中所有的邊,這樣圖中每個頂點都是一個單獨的連通塊,一共有n個連通塊
  • 對所有邊按邊權從小到大進行排序
  • 按邊權從小到大測試所有邊,如果當前測試邊所連接的兩個頂點不在同一個連通塊中,則把這條測試邊加入當前最小生成樹中,否則,將邊捨棄。
  • 重複執行上一步驟,直到最小生成樹中的邊數等於總頂點數減一 或者測試完所有邊時結束;如果結束時,最小生成樹的邊數小於總頂點數減一,說明該圖不連通。

請看下面的Gif圖!

算法實現:

#include<iostream>
#include<string>
#include<vector>
#include<queue>
#define INF 100000
#define MaxVertex 105
typedef int Vertex; 
int G[MaxVertex][MaxVertex];
int parent[MaxVertex];   // 並查集最小生成樹 
int Nv;    // 結點 
int Ne;    // 邊 
int sum;  // 權重和 
using namespace std; 
struct Node{
    Vertex v1;
    Vertex v2;
    int weight; // 權重 
    // 重載運算符成最大堆 
    bool operator < (const Node &a) const
    {
        return weight>a.weight;
    }
};
vector<Node> MST;  // 最小生成樹 
priority_queue<Node> q;   // 最小堆 

// 初始化圖信息 
void build(){
    Vertex v1,v2;
    int w;
    cin>>Nv>>Ne;
    for(int i=1;i<=Nv;i++){
        for(int j=1;j<=Nv;j++)
            G[i][j] = 0;  // 初始化圖
        parent[i] = -1;
    }
    // 初始化點
    for(int i=0;i<Ne;i++){
        cin>>v1>>v2>>w;
        struct Node tmpE;
        tmpE.v1 = v1;
        tmpE.v2 = v2;
        tmpE.weight = w;
        q.push(tmpE); 
    }
}

//  路徑壓縮查找 
int Find(int x){
    if(parent[x] < 0)
        return x;
    else
        return parent[x] = Find(parent[x]);
} 

//  按秩歸併 
void Union(int x1,int x2){
    if(parent[x1] < parent[x2]){
        parent[x1] += parent[x2];
        parent[x2] = x1;
    }else{
        parent[x2] += parent[x1];
        parent[x1] = x2;
    }
} 

void Kruskal(){
    // 最小生成樹的邊不到 Nv-1 條且還有邊 
    while(MST.size()!= Nv-1 && !q.empty()){
        Node E = q.top();  // 從最小堆取出一條權重最小的邊
        q.pop(); // 出隊這條邊 
        if(Find(E.v1) != Find(E.v2)){  // 檢測兩條邊是否在同一集合 
            sum += E.weight; 
            Union(E.v1,E.v2);     // 並起來 
            MST.push_back(E);
        }
    }
    
} 


void output(){
    cout<<"被收錄順序:"<<endl; 
    for(Vertex i=0;i<Nv;i++)
        cout<<MST[i].weight<<" ";
    cout<<"權重和為:"<<sum<<endl; 
    for(Vertex i=1;i<=Nv;i++)
        cout<<parent[i]<<" ";
    cout<<endl;
}


int main(){
    build();
    Kruskal();
    output();
    return 0;
} 

關於kruskal算法更詳細的講解

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

【原創】(十一)Linux內存管理slub分配器

背景

  • Read the fucking source code! –By 魯迅
  • A picture is worth a thousand words. –By 高爾基

說明:

  1. Kernel版本:4.14
  2. ARM64處理器,Contex-A53,雙核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

之前的文章分析的都是基於頁面的內存分配,而小塊內存的分配和管理是通過塊分配器來實現的。目前內核中,有三種方式來實現小塊內存分配:slab, slub, slob,最先有slab分配器,slub/slob分配器是改進版,slob分配器適用於小內存嵌入式設備,而slub分配器目前已逐漸成為主流塊分配器。接下來的文章,就是以slub分配器為目標,進一步深入。

先來一個初印象:

2. 數據結構

有四個關鍵的數據結構:

  • struct kmem_cache:用於管理SLAB緩存,包括該緩存中對象的信息描述,per-CPU/Node管理slab頁面等;
    關鍵字段如下:
/*
 * Slab cache management.
 */
struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;       //每個CPU slab頁面
    /* Used for retriving partial slabs etc */
    unsigned long flags;
    unsigned long min_partial;
    int size;       /* The size of an object including meta data */
    int object_size;    /* The size of an object without meta data */
    int offset;     /* Free pointer offset. */
#ifdef CONFIG_SLUB_CPU_PARTIAL
    /* Number of per cpu partial objects to keep around */
    unsigned int cpu_partial;
#endif
    struct kmem_cache_order_objects oo;     //該結構體會描述申請頁面的order值,以及object的個數

    /* Allocation and freeing of slabs */
    struct kmem_cache_order_objects max;
    struct kmem_cache_order_objects min;
    gfp_t allocflags;   /* gfp flags to use on each alloc */
    int refcount;       /* Refcount for slab cache destroy */
    void (*ctor)(void *);           // 對象構造函數
    int inuse;      /* Offset to metadata */
    int align;      /* Alignment */
    int reserved;       /* Reserved bytes at the end of slabs */
    int red_left_pad;   /* Left redzone padding size */
    const char *name;   /* Name (only for display!) */
    struct list_head list;  /* List of slab caches */       //kmem_cache最終會鏈接在一個全局鏈表中
    struct kmem_cache_node *node[MAX_NUMNODES];     //Node管理slab頁面
};
  • struct kmem_cache_cpu:用於管理每個CPU的slab頁面,可以使用無鎖訪問,提高緩存對象分配速度;
struct kmem_cache_cpu {
    void **freelist;    /* Pointer to next available object */                  //指向空閑對象的指針
    unsigned long tid;  /* Globally unique transaction id */                
    struct page *page;  /* The slab from which we are allocating */     //slab緩存頁面
#ifdef CONFIG_SLUB_CPU_PARTIAL
    struct page *partial;   /* Partially allocated frozen slabs */
#endif
#ifdef CONFIG_SLUB_STATS
    unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
  • struct kmem_cache_node:用於管理每個Node的slab頁面,由於每個Node的訪問速度不一致,slab頁面由Node來管理;
/*
 * The slab lists for all objects.
 */
struct kmem_cache_node {
    spinlock_t list_lock;

#ifdef CONFIG_SLUB
    unsigned long nr_partial;    //slab頁表數量
    struct list_head partial;       //slab頁面鏈表
#ifdef CONFIG_SLUB_DEBUG
    atomic_long_t nr_slabs;
    atomic_long_t total_objects;
    struct list_head full;
#endif
#endif
};
  • struct page:用於描述slab頁面struct page結構體中很多字段都是通過union聯合體進行復用的。
    struct page結構中,用於slub的成員如下:
struct page {
    union {
       ...
        void *s_mem;            /* slab first object */
       ...
    };
    
    /* Second double word */
    union {
       ...
        void *freelist;     /* sl[aou]b first free object */
       ...
    };
    
    union {
       ...
        struct {
            union {
              ...
                struct {            /* SLUB */
                    unsigned inuse:16;
                    unsigned objects:15;
                    unsigned frozen:1;
                };
                ...
            };
       ...
        };       
    };   
    
    /*
     * Third double word block
     */
    union {
       ...
        struct {        /* slub per cpu partial pages */
            struct page *next;  /* Next partial slab */
#ifdef CONFIG_64BIT
            int pages;  /* Nr of partial slabs left */
            int pobjects;   /* Approximate # of objects */
#else
            short int pages;
            short int pobjects;
#endif
        };

        struct rcu_head rcu_head;   /* Used by SLAB
                         * when destroying via RCU
                         */
    };
    ...
        struct kmem_cache *slab_cache;  /* SL[AU]B: Pointer to slab */    
    ...
}

圖來了:

3. 流程分析

針對Slub的使用,可以從三個維度來分析:

  1. slub緩存創建
  2. slub對象分配
  3. slub對象釋放

下邊將進一步分析。

3.1 kmem_cache_create

在內核中通過kmem_cache_create接口來創建一個slab緩存

先看一下這個接口的函數調用關係圖:

  1. kmem_cache_create完成的功能比較簡單,就是創建一個用於管理slab緩存kmem_cache結構,並對該結構體進行初始化,最終添加到全局鏈表中。kmem_cache結構體初始化,包括了上文中分析到的kmem_cache_cpukmem_cache_node兩個字段結構。

  2. 在創建的過程中,當發現已有的slab緩存中,有存在對象大小相近,且具有兼容標誌的slab緩存,那就只需要進行merge操作並返回,而無需進一步創建新的slab緩存

  3. calculate_sizes函數會根據指定的force_order或根據對象大小去計算kmem_cache結構體中的size/min/oo等值,其中kmem_cache_order_objects結構體,是由頁面分配order值和對象數量兩者通過位域拼接起來的。

  4. 在創建slab緩存的時候,有一個先雞后蛋的問題:kmem_cache結構體來管理一個slab緩存,而創建kmem_cache結構體又是從slab緩存中分配出來的對象,那麼這個問題是怎麼解決的呢?可以看一下kmem_cache_init函數,內核中定義了兩個靜態的全局變量kmem_cachekmem_cache_node,在kmem_cache_init函數中完成了這兩個結構體的初始化之後,相當於就是創建了兩個slab緩存,一個用於分配kmem_cache結構體對象的緩存池,一個用於分配kmem_cache_node結構體對象的緩存池。由於kmem_cache_cpu結構體是通過__alloc_percpu來分配的,因此不需要創建一個相關的slab緩存

3.2 kmem_cache_alloc

kmem_cache_alloc接口用於從slab緩存池中分配對象。

看一下大體的調用流程圖:

從上圖中可以看出,分配slab對象與Buddy System中分配頁面類似,存在快速路徑和慢速路徑兩種,所謂的快速路徑就是per-CPU緩存,可以無鎖訪問,因而效率更高。

整體的分配流程大體是這樣的:優先從per-CPU緩存中進行分配,如果per-CPU緩存中已經全部分配完畢,則從Node管理的slab頁面中遷移slab頁per-CPU緩存中,再重新分配。當Node管理的slab頁面也不足的情況下,則從Buddy System中分配新的頁面,添加到per-CPU緩存中。

還是用圖來說明更清晰,分為以下幾步來分配:

  1. fastpath
    快速路徑下,以原子的方式檢索per-CPU緩存的freelist列表中的第一個對象,如果freelist為空並且沒有要檢索的對象,則跳入慢速路徑操作,最後再返回到快速路徑中重試操作。

  2. slowpath-1
    將per-CPU緩存中page指向的slab頁中的空閑對象遷移到freelist中,如果有空閑對象,則freeze該頁面,沒有空閑對象則跳轉到slowpath-2

  3. slowpath-2
    將per-CPU緩存中partial鏈表中的第一個slab頁遷移到page指針中,如果partial鏈表為空,則跳轉到slowpath-3

  4. slowpath-3
    將Node管理的partial鏈表中的slab頁遷移到per-CPU緩存中的page中,並重複第二個slab頁將其添加到per-CPU緩存中的partial鏈表中。如果遷移的slab中空閑對象超過了kmem_cache.cpu_partial的一半,則僅遷移slab頁,並且不再重複。
    如果每個Node的partial鏈表都為空,跳轉到slowpath-4

  5. slowpath-4
    Buddy System中獲取頁面,並將其添加到per-CPU的page中。

3.2 kmem_cache_free

kmem_cache_free的操作,可以看成是kmem_cache_alloc的逆過程,因此也分為快速路徑和慢速路徑兩種方式,同時,慢速路徑中又分為了好幾種情況,可以參考kmem_cache_alloc的過程。

調用流程圖如下:

效果如下:

  1. 快速路徑釋放
    快速路徑下,直接將對象返回到freelist中即可。

  2. put_cpu_partial
    put_cpu_partial函數主要是將一個剛freeze的slab頁,放入到partial鏈表中。
    put_cpu_partial函數中調用unfreeze_partials函數,這時候會將per-CPU管理的partial鏈表中的slab頁面添加到Node管理的partial鏈表的尾部。如果超出了Node的partial鏈表,溢出的slab頁面中沒有分配對象的slab頁面將會返回到夥伴系統。

  3. add_partial
    添加slab頁到Node的partial鏈表中。

  4. remove_partial
    從Node的partial鏈表移除slab頁。

具體釋放的流程走哪個分支,跟對象的使用情況,partial鏈表的個數nr_partial/min_partial等相關,細節就不再深入分析了。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

.NET高級特性-Emit(1)

  在這個大數據/雲計算/人工智能研發普及的時代,Python的崛起以及Javascript的前後端的侵略,程序員與企業似乎越來越青睞動態語言所帶來的便捷性與高效性,即使靜態語言在性能,錯誤檢查等方面的優於靜態語言。對於.NETer來說,.NET做為一門靜態語言,我們不僅要打好.NET的基本功,如基本類型/語法/底層原理/錯誤檢查等知識,也要深入理解.NET的一些高級特性,來為你的工作減輕負擔和提高代碼質量。

  ok,咱們今天開始聊一聊.NET中的Emit。

一、什麼是Emit?

  Emit含義為發出、產生的含義,這是.NET中的一組類庫,命名空間為System.Reflection.Emit,幾乎所有的.NET版本(Framework/Mono/NetCore)都支持Emit,可以實現用C#代碼生成代碼的類庫

二、Emit的本質

  我們知道.NET可以由各種語言進行編寫,比如VB,C++等,當然絕大部分程序員進行.NET開發都是使用C#語言進行的,這些語言都會被各自的語言解釋器解釋為IL語言並執行,而Emit類庫的作用就是用這些語言來編寫生成IL語言,並交給CLR(公共語言運行時)進行執行。

  我們先來看看IL語言長什麼樣子:

  (1) 首先我們創建一個Hello,World程序

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }

  (2) 將程序編譯成dll文件,我們可以看到在開發目錄下生成了bin文件夾

  

  (3) 向下尋找,我們可以看到dll文件已經生成,筆者使用netcore3進行開發,故路徑為bin/Debug/netcoreapp3.0

  

  (4) 這時候,我們就要祭出我們的il查看神器了,ildasm工具

  

  如何找到這個工具?打開開始菜單,找到Visual Studio文件夾,打開Developer Command Prompt,在打開的命令行中鍵入ildasm回車即可,筆者使用vs2019進行演示,其它vs版本操作方法均一致

  

 

 

 

 

 

 

   (5) 在dasm菜單欄選擇文件->打開,選擇剛剛生成的dll文件

  

 

 

   (6) 即可查看生成il代碼

  

 

  有了ildasm的輔助,我們就能夠更好的了解IL語言以及如何編寫IL語言,此外,Visual Studio中還有許多插件支持查看il代碼,比如JetBrains出品的Resharper插件等,如果覺得筆者方式較為麻煩可以使用以上插件查看il代碼

三、理解IL代碼

  在上一章節中,我們理解了Emit的本質其實就是用C#來編寫IL代碼,既然要編寫IL代碼,那麼我們首先要理解IL代碼是如何進行工作的,IL代碼是如何完成C#當中的順序/選擇/循環結構的,是如何實現類的定義/字段的定義/屬性的定義/方法的定義的。

  IL代碼是一種近似於指令式的代碼語言,與彙編語言比較相近,所以習慣於寫高級語言的.NETer來說比較難以理解

  讓我們來看看Hello,World程序的IL代碼:

IL_0000:  nop
IL_0001:  ldstr      "Hello World!"
IL_0006:  call       void [System.Console]System.Console::WriteLine(string)
IL_000b:  nop
IL_000c:  ret

  我們可以把IL代碼看成棧的運行

  第一條指令,nop表示不做任何事情,表示代碼不做任何事情

  第二條指令,ldstr表示將字符串放入棧中,字符串的值為“Hello,World!”

  第三條指令,call表示調用方法,參數為調用方法的方法信息,並把返回的結構壓入棧中,使用的參數為之前已經入棧的“Hello World!”,以此類推,如果方法有n個參數,那麼他就會調取棧中n個數據,並返回一個結果放回棧中

  第四條指令,nop表示不做任何事情

  第五條指令,ret表示將棧中頂部的數據返回,如果方法定義為void,則無返回值

  關於Hello,world程序IL的理解就說到這裏,更多的指令含義讀者可以參考微軟官方文檔,筆者之後也會繼續對Emit進行講解和Emit的應用

四、用Emit類庫編寫IL代碼

  既然IL代碼咱們理解的差不多了,咱們就開始嘗試用C#來寫IL代碼了,有了IL代碼的參考,咱們也可以依葫蘆畫瓢的把代碼寫出來了

  (1) 引入Emit命名空間

using System.Reflection.Emit;

  (2) 首先我們定義一個Main方法,入參無,返回類型void

//定義方法名,返回類型,輸入類型
var method = new DynamicMethod("Main", null, Type.EmptyTypes);

  (3) 生成IL代碼

//生成IL代碼
var ilGenerator = method.GetILGenerator();
ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Ldstr,"Hello World!");
ilGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) })); //尋找Console的WriteLine方法
ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Ret);

  (4) 創建委託並調用

//創建委託
var helloWorldMethod = method.CreateDelegate(typeof(Action)) as Action;
helloWorldMethod.Invoke();

  (5)運行,即輸出Hello World!

五、小結

  Emit的本質是使用高級語言生成IL代碼,進而進行調用的的一組類庫,依賴Emit我們可以實現用代碼生成代碼的操作,即編程語言的自舉,可以有效彌補靜態語言的靈活性的缺失。

  Emit的性能非常好,除了第一次構建IL代碼所需要時間外,之後只要將操作緩存在計算機內存中,速度與手寫代碼相差無幾

  有許多著名.NET類庫均依賴於Emit:

  (.NET JSON操作庫)Json.NET/Newtonsoft.Json:

  (輕量ORM)Dapper:

  (ObjectToObjectMapper)EmitMapper:

  (AOP庫)Castle.DynamicProxy:

  學習Emit:

  .NET官方文檔:

  .NET API瀏覽器:

  之後作者將繼續講解.NET Emit的相關內容和應用,感謝閱讀

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

實現 Redis 協議解析器

本文是 《用 Golang 實現一個 Redis》系列文章第二篇,本文將分別介紹 以及 的實現,若您對協議有所了解可以直接閱讀協議解析器部分。

Redis 通信協議

Redis 自 2.0 版本起使用了統一的協議 RESP (REdis Serialization Protocol),該協議易於實現,計算機可以高效的進行解析且易於被人類讀懂。

RESP 是一個二進制安全的文本協議,工作於 TCP 協議上。客戶端和服務器發送的命令或數據一律以 \r\n (CRLF)結尾。

RESP 定義了5種格式:

  • 簡單字符串(Simple String): 服務器用來返回簡單的結果,比如”OK”。非二進制安全,且不允許換行。
  • 錯誤信息(Error): 服務器用來返回簡單的結果,比如”ERR Invalid Synatx”。非二進制安全,且不允許換行。
  • 整數(Integer): llenscard等命令的返回值, 64位有符號整數
  • 字符串(Bulk String): 二進制安全字符串, get 等命令的返回值
  • 數組(Array, 舊版文檔中稱 Multi Bulk Strings): Bulk String 數組,客戶端發送指令以及lrange等命令響應的格式

RESP 通過第一個字符來表示格式:

  • 簡單字符串:以”+” 開始, 如:”+OK\r\n”
  • 錯誤:以”-” 開始,如:”-ERR Invalid Synatx\r\n”
  • 整數:以”:”開始,如:”:1\r\n”
  • 字符串:以 $ 開始
  • 數組:以 * 開始

Bulk String有兩行,第一行為 $+正文長度,第二行為實際內容。如:

$3\r\nSET\r\n

Bulk String 是二進制安全的可以包含任意字節,就是說可以在 Bulk String 內部包含 “\r\n” 字符(行尾的CRLF被隱藏):

$4
a\r\nb

$-1 表示 nil, 比如使用 get 命令查詢一個不存在的key時,響應即為$-1

Array 格式第一行為 “*”+數組長度,其後是相應數量的 Bulk String。如, ["foo", "bar"]的報文:

*2
$3
foo
$3
bar

客戶端也使用 Array 格式向服務端發送指令。命令本身將作為第一個參數,如 SET key value指令的RESP報文:

*3
$3
SET
$3
key
$5
value

將換行符打印出來:

*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

協議解析器

我們在 一文中已經介紹過TCP服務器的實現,協議解析器將實現其 Handler 接口充當應用層服務器。

協議解析器將接收 Socket 傳來的數據,並將其數據還原為 [][]byte 格式,如 "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\value\r\n" 將被還原為 ['SET', 'key', 'value']

本文完整代碼:

來自客戶端的請求均為數組格式,它在第一行中標記報文的總行數並使用CRLF作為分行符。

bufio 標準庫可以將從 reader 讀到的數據緩存到 buffer 中,直至遇到分隔符或讀取完畢后返回,所以我們使用 reader.ReadBytes('\n') 來保證每次讀取到完整的一行。

需要注意的是RESP是二進制安全的協議,它允許在正文中使用CRLF字符。舉例來說 Redis 可以正確接收並執行SET "a\r\nb" 1指令, 這條指令的正確報文是這樣的:

*3  
$3
SET
$4
a\r\nb 
$7
myvalue

ReadBytes 讀取到第五行 “a\r\nb\r\n”時會將其誤認為兩行:

*3  
$3
SET
$4
a  // 錯誤的分行
b // 錯誤的分行
$7
myvalue

因此當讀取到第四行$4后, 不應該繼續使用 ReadBytes('\n') 讀取下一行, 應使用 io.ReadFull(reader, msg) 方法來讀取指定長度的內容。

msg = make([]byte, 4 + 2) // 正文長度4 + 換行符長度2
_, err = io.ReadFull(reader, msg)

定義 Client 結構體作為客戶端抽象:

type Client struct {
    /* 與客戶端的 Tcp 連接 */
    conn   net.Conn

    /* 
     * 帶有 timeout 功能的 WaitGroup, 用於優雅關閉
     * 當響應被完整發送前保持 waiting 狀態, 阻止鏈接被關閉
     */
    waitingReply wait.Wait

    /* 標記客戶端是否正在發送指令 */ 
    sending atomic.AtomicBool
    
    /* 客戶端正在發送的參數數量, 即 Array 第一行指定的數組長度 */
    expectedArgsCount uint32
    
    /* 已經接收的參數數量, 即 len(args)*/ 
    receivedCount uint32
    
    /*
     * 已經接收到的命令參數,每個參數由一個 []byte 表示
     */
    args [][]byte
}

定義解析器:

type Handler struct {

    /* 
     * 記錄活躍的客戶端鏈接 
     * 類型為 *Client -> placeholder 
     */
    activeConn sync.Map 

    /* 數據庫引擎,執行指令並返回結果 */
    db db.DB

    /* 關閉狀態標誌位,關閉過程中時拒絕新建連接和新請求 */
    closing atomic.AtomicBool 
}

接下來可以編寫主要部分了:

func (h *Handler)Handle(ctx context.Context, conn net.Conn) {
    if h.closing.Get() {
        // 關閉過程中不接受新連接
        _ = conn.Close()
    }

    /* 初始化客戶端狀態 */
    client := &Client {
        conn:   conn,
    }
    h.activeConn.Store(client, 1)

    reader := bufio.NewReader(conn)
    var fixedLen int64 = 0 // 將要讀取的 BulkString 的正文長度
    var err error
    var msg []byte
    for {
        /* 讀取下一行數據 */ 
        if fixedLen == 0 { // 正常模式下使用 CRLF 區分數據行
            msg, err = reader.ReadBytes('\n')
            // 判斷是否以 \r\n 結尾
            if len(msg) == 0 || msg[len(msg) - 2] != '\r' {
                errReply := &reply.ProtocolErrReply{Msg:"invalid multibulk length"}
                _, _ =  client.conn.Write(errReply.ToBytes())
            }
        } else { // 當讀取到 BulkString 第二行時,根據給出的長度進行讀取
            msg = make([]byte, fixedLen + 2)
            _, err = io.ReadFull(reader, msg)
            // 判斷是否以 \r\n 結尾
            if len(msg) == 0 || 
              msg[len(msg) - 2] != '\r' ||  
              msg[len(msg) - 1] != '\n'{
                errReply := &reply.ProtocolErrReply{Msg:"invalid multibulk length"}
                _, _ =  client.conn.Write(errReply.ToBytes())
            }
            // Bulk String 讀取完畢,重新使用正常模式
            fixedLen = 0 
        }
        // 處理 IO 異常
        if err != nil {
            if err == io.EOF || err == io.ErrUnexpectedEOF {
                logger.Info("connection close")
            } else {
                logger.Warn(err)
            }
            _ = client.Close()
            h.activeConn.Delete(client)
            return // io error, disconnect with client
        }

        /* 解析收到的數據 */
        if !client.sending.Get() { 
            // sending == false 表明收到了一條新指令
            if msg[0] == '*' {
                // 讀取第一行獲取參數個數
                expectedLine, err := strconv.ParseUint(string(msg[1:len(msg)-2]), 10, 32)
                if err != nil {
                    _, _ = client.conn.Write(UnknownErrReplyBytes)
                    continue
                }
                // 初始化客戶端狀態
                client.waitingReply.Add(1) // 有指令未處理完成,阻止服務器關閉
                client.sending.Set(true) // 正在接收指令中
                // 初始化計數器和緩衝區 
                client.expectedArgsCount = uint32(expectedLine) 
                client.receivedCount = 0
                client.args = make([][]byte, expectedLine)
            } else {
                // TODO: text protocol
            }
        } else {
            // 收到了指令的剩餘部分(非首行)
            line := msg[0:len(msg)-2] // 移除換行符
            if line[0] == '$' { 
                // BulkString 的首行,讀取String長度
                fixedLen, err = strconv.ParseInt(string(line[1:]), 10, 64)
                if err != nil {
                    errReply := &reply.ProtocolErrReply{Msg:err.Error()}
                    _, _ = client.conn.Write(errReply.ToBytes())
                }
                if fixedLen <= 0 {
                    errReply := &reply.ProtocolErrReply{Msg:"invalid multibulk length"}
                    _, _ = client.conn.Write(errReply.ToBytes())
                }
            } else { 
                // 收到參數
                client.args[client.receivedCount] = line
                client.receivedCount++
            }


            // 一條命令發送完畢
            if client.receivedCount == client.expectedArgsCount {
                client.sending.Set(false)

                // 執行命令並響應
                result := h.db.Exec(client.args)
                if result != nil {
                    _, _ = conn.Write(result.ToBytes())
                } else {
                    _, _ = conn.Write(UnknownErrReplyBytes)
                }

                // 重置客戶端狀態,等待下一條指令
                client.expectedArgsCount = 0
                client.receivedCount = 0
                client.args = nil
                client.waitingReply.Done()
            }
        }
    }
}

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

I/O多路復用模型

背景

在文章中提到了五種I/O模型,其中前四種:阻塞模型、非阻塞模型、信號驅動模型、I/O復用模型都是同步模型;還有一種是異步模型。

想寫一個系列的文章,介紹從I/O多路復用到異步編程和RPC框架,整個演進過程,這一系列可能包括:

  1. Reactor和Proactor模型
  2. 為什麼需要異步編程
  3. enable_shared_from_this用法分析
  4. 網絡通信庫和RPC

為什麼有多路復用?

多路復用技術要解決的是“通信”問題,解決核心在於“同步事件分離器”(de-multiplexer),linux系統帶有的分離器select、poll、epoll網上介紹的比較多,大家可以看看這篇介紹的不錯的文章:。通信的一方想要知道另一方的狀態(以決定自己做什麼),有兩種方法: 一是輪詢,二是消息通知。

輪詢

輪詢的一種典型的實現可能是這樣的:當然這裏的epoll_wait()也可以使用poll()或者select()替換。

whiletrue) {
    active_stream[] = epoll_wait(epollfd)
    for i in active_stream[] {
        read or write till
    }
}

輪詢方式主要存在以下不足:

  • 增加系統開銷。無論是任務輪詢還是定時器輪詢都需要消耗對應的系統資源。
  • 無法及時感知設備狀態變化。在輪詢間隔內的設備狀態變化只有在下次輪詢時才能被發現,這將無法滿足對實時性敏感的應用場合。
  • 浪費CPU資源。無論設備是否發生狀態改變,輪詢總在進行。在實際情況中,大多數設備的狀態改變通常不會那麼頻繁,輪詢空轉將白白浪費CPU時間片。

消息通知

其實現方式通常是: “阻塞-通知”機制。阻塞會導致一個任務(task_struct,進程或者線程)只能處理一個”I/O流”或者類似的操作,要處理多個,就要多個任務(需要多個進程或線程),因此靈活性上又不如輪詢(一個任務足夠),很矛盾。

 

select、poll、epoll對比

矛盾的根源就是”一”和”多”的矛盾: 希望一個任務處理多個對象,同時避免處理阻塞-通知機制的內部細節。解決方案是多路復用(muliplex)。多路復用有3種基本方案,select()/poll()/epoll(),都是來解決這一矛盾的。

  • 通知代理: 用戶把需要關心的對象註冊給select()/poll()/epoll()函數。
  • 一對多: 所有的被關心的對象,只要有一個對象有了通知事件,select()/poll()/epoll()就會結束阻塞狀態。
  • 方便性: 用戶(程序員)不用再關心如何阻塞和被通知,以及哪些情況下會有通知產生。這件事情已經由上述幾個系統調用做了,用戶只需要實現”通知來了我該做什麼”。

 

那麼上面3個系統調用的區別是什麼呢?
第一個select(),結合了輪詢和阻塞兩種方式,沒有問題,每次有一個對象事件發生的時候,select()只是知道有事件發生了,具體是哪個對象發生的,不知道,需要從頭到尾輪詢一遍,複雜度是O(n)。poll函數相對select函數變化不大,只是提升了最大的可輪詢的對象個數。epoll函數把時間複雜度降到O(1)。

 

為什麼select慢而epoll效率高?
select()之所以慢,有幾個原因: select()的參數是一個FD數組,意味着每次select調用,都是一次新的註冊-阻塞-回調,每次select都要把一個數組從用戶空間拷貝到內核空間,內核檢測到某個對象狀態變化並寫入后,再從內核空間拷貝回用戶空間,select再把這個數組讀取一遍,並返回。這個過程非常低效。

epoll的解決方案相當於是一種對select()的算法優化: 它把select()一個函數做的事情分解成了3步,首先epoll_create()創建一個epollfd對象(相當於一個池子),然後所有被監聽的fd通過epoll_ctrl()註冊到這個池子,也就是為每個fd指定了一個內部的回調函數(這樣,就沒有了每次調用時的來回拷貝,用戶空間的數組到內核空間只有這一次拷貝)。epoll_wait阻塞等待。在內核態有一個和epoll_wait對應的函數調用,把就緒的fd,填入到一個就緒列表中,而epoll_wait讀取這個就緒列表,做到了快速返回(O(1))。

詳細的對比可以參考select、poll、epoll之間的區別總結:

 

有了上面的原理介紹,這裏舉例來說明下epoll到底是怎麼使用的,加深理解。舉兩個例子:

一個是比較簡單的父子進程通信的例子,單個小程序,不需要跑多個應用實例,不需要用戶輸入。
一個是比較實戰的socket+epoll,畢竟現實案例中哪有兩個父子進程間通訊這麼簡單的應用場景。

有了多路復用,難道還不夠?

有了I/O復用,有了epoll已經可以使服務器併發幾十萬連接的同時,維持高TPS了,難道這還不夠嗎?答案是,技術層面足夠了,但在軟件工程層面卻是不夠的。例如,總要有個for循環去調用epoll,總來處理epoll的返回,這是每次都要重複的工作。for循環體裏面寫什麼—-通知返回之後,做事情的程序最好能以一種回調的機制,提供一個編程框架,讓程序更有結構一些。另一方面,如果希望每個事件通知之後,做的事情能有機會被代理到某個線程裏面去單獨運行,而線程完成的狀態又能通知回主任務,那麼”異步”的進制就必須被引入。

所以,還有兩個問題要解決,一是”編程框架”,一是”異步”。我們先看幾個目前流行的框架,大部分框架已經包含了某種異步的機制。我們接下來的篇章將介紹“編程框架”和“異步I/O模型”。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!