面向對象第二單元總結 - 之 - 吾梯永不停
- 面向對象第二單元總結 - 之 - 吾梯永不停
- 一、設計策略
- 1.1 調度策略
- 1.2 多綫程協同與同步控制
- 1.3 電梯進程與請求隊列接口概述
- 二、SOLID原則 - 之於 - 作業3
- 2.1 SRP (Single Responsibility Priciple)
- 2.2 OCP (Open Close Principle)
- 2.3 LSP (Liskov Substitution Principle)
- 2.4 ISP (Interface Segregation Principle)
- 2.5 DIP (Dependency Inversion Principle)
- 三、程序結構分析
- 作業1
- 作業2
- 作業3
- 四、bug分析
- 五、發現他人bug策略
- 六、心得體會
- 6.1 綫程安全
- 6.2 設計原則
- 七、寫在最後
- 一、設計策略
一、設計策略
1.1 調度策略
三次作業我都采用了同樣的電梯調度算法——LOOK算法。什麽是LOOK算法呢?這要先從SCAN算法説起。
- SCAN算法
SCAN算法是一種按照樓層順序一次服務請求的算法。它讓電梯在最底層和最頂層之間連續往返運行,在運行過程中相應處在電梯運行方向相同的各樓層上的請求。SCAN算法的平均響應時間比SSTF算法長,但是響應時間方差比SSTF算法小。從統計學角度來講,SCAN算法要比SSTF算法穩定。
如果以指導書上的標準算法ALS的角度來看的話,其實可以將SCAN算法理解爲:在電梯中永遠都有一個虛擬的主請求,即電梯上行的時候主請求就是一個從最底層到最頂層的虛擬請求、電梯下行的時候主請求就是一個從最頂層到最底層的虛擬請求。此時一切客觀存在的真實請求便都是這個虛擬的主請求的可捎帶請求。
- LOOK算法
LOOK算法是對SCAN算法的一種改進。對LOOK算法而言,電梯同樣在最底層和最頂層之間運行,但當電梯發現當前電梯所移動的方向上不再有請求時(電梯內沒有到達請求,電梯外沒有呼梯請求),電梯會立即改變運行方向。而SCAN算法會繼續運行到最底層或最頂層才改變運行方向。
使用LOOK算法的一個原因就是感覺這樣的調度算法寫起來較爲簡單:只需要寫出電梯的反向條件和開門條件,讓電梯自行去接人,這樣就是一個比較完整的調度算法,而不需要考慮設置調度器爲電梯分配請求。
使用LOOK算法的另一個原因就是因爲LOOK算法對捎帶及其友好。LOOK算法對所有請求一視同仁,不會將請求分爲奇奇怪怪的主請求和捎帶請求。只要電梯容量允許,就可以捎帶任何的請求。此時,捎帶的條件就及其顯然了。
1.2 多綫程協同與同步控制
在本單元作業中,我使用了類似於生產者消費者的模式,但也根據具體需要對其進行了一點調整。在我的程序中有兩類綫程:主綫程——負責輸入、電梯綫程——負責實現請求。我的程序中還有這兩類綫程互相交互的托盤類:請求隊列、結束標識等。如果有多部電梯的話,多部電梯之間沒有交互,彼此透明。
對於多部電梯的情況,我認爲設計一個調度器本身就不符合LOOK算法的思想。LOOK電梯的思想是:電梯自行決定運行方向,並做到能接人則接人。因此無需對電梯施加過多的影響,無爲而治即可。僅僅在作業2、可能有多部電梯同時運行的情況下,爲了使多部電梯均勻的分佈在所有樓層之間,我設計了一個初始方向和初始延遲時間,效果比多部電梯同時從一層開始掃描好很多。但作業3中我便放棄了這一做法,因爲不同類型的電梯初始僅一個,沒有必要刻意錯開。
在本單元作業多綫程同步部分的相關代碼中,我沒有使用wait方法。有同學可能會比較驚異于,不用wait()方法,難道不會造成暴力輪詢、從而導致CTLE嗎?事實上,我真的從來沒有遇到過CTLE。之所以不使用wait方法,這其實是與我的電梯調度策略有緊密的關聯性,同時我的請求隊列的接口也很好的提供了相關的邏輯實現。具體參見1.3 電梯進程與請求隊列接口概述。
1.3 電梯進程與請求隊列接口概述
对于LOOK算法,即使电梯內外都沒有請求,電梯也不會停下來等待請求,而是根據當前方向上的請求情況繼續決定向哪個方向運行。這種情況下,電梯到達某個樓層之後會進行一個返回值爲boolean的hasRequest方法的判斷。當請求隊列中沒有請求的話,只要返回false即可,此時電梯正常前行,實在不需要使用wait方法、等待下一個請求輸入。事實上,即使當請求隊列中有請求,如果該請求不是在電梯當前所在的樓層,在判斷時也會返回false。
電梯內部也有一個內部隊列。電梯開門後,電梯可以將主請求隊列當前樓層存在的請求取出並添加到電梯的內部請求隊列中,這代表了人員進入電梯;電梯還可以將當前内部請求隊列中的請求移出,代表人員離開電梯。該內部請求隊列和主請求隊列其實是類似的,同樣通過一個返回值爲boolean的hasRequest方法來判斷是否有人員要離開。
當上述兩個請求隊列中的只要有一個hasRequest方法返回值爲true,電梯就需要在當前層開門。開門後,電梯將通過請求隊列的getRequest方法取出需要上下電梯的請求,並進行執行(輸出)。如果所有當前層可執行的請求都已取出,繼續調用getRequest方法將會得到null。
電梯調度中的一大關鍵是電梯進程中的needReverse方法。該方法通過調用請求隊列中的接口,實現了對電梯反向的邏輯判斷。這也是區別與SCAN算法的重要之處。
關於請求隊列,要寫的有點多、且比較麻煩。在作業1中我采用了Arraylist
來存儲,而在作業2、3中我采用了HashMap
,將請求與樓層進行綁定。比較詭異的一點是,在主請求隊列中,我采用了FROM與Request綁定的方式;在內部請求隊列中,我采用了TO與Request綁定的方式。這導致了兩者都由同樣的類來實現、共用同樣的方法顯得非常的奇怪。出現的情況就是,同樣的方法對於主請求隊列和內部請求隊列來説含義可能是不一樣的。所以我也説不好,這樣的代碼復用到底好不好。在作業1和作業2中我采用了同一個類來實現;而在作業3中,根據需要,我便將兩個隊列類徹底分開了。
二、SOLID原則 - 之於 - 作業3
2.1 SRP (Single Responsibility Priciple)
SRP原則意指每個類或方法都只有一個明確的職責。這一點上我認爲我的作業3還算可以。主類只關注於輸入、電梯類只關注與其運行與開關門、綫程交互所需的容器我全部集中在Dispatcher類中進行集中管理、主請求隊列和內部請求隊列只關心隊列自身請求的輸入與輸出。此外還有一個StopFloors類統一存放並管理不同類型電梯所能停靠樓層的全部靜態數據、TargetFloorCal類統一存放計算請求目的樓層(換乘或直達皆可用)的全部靜態方法。每個類職責清晰,沒有出現一個類職責不明確的情況。
2.2 OCP (Open Close Principle)
OCP原則意指無需修改已有實現(close),而是通過擴展來增加新功能(open)。這一點我得承認我的第三次作業做的不好。我的第三次作業是在第二次作業的基礎上直接進行的修改,有些地方改得也有點亂了;此外,由於我的電梯是自行調度,核心調度邏輯都集中在電梯類中,因此如果涉及到功能的擴展的話,必然需要對底層電梯内部的邏輯進行修改,而無法僅僅通過新增一些調度相關的類來實現功能擴展。
2.3 LSP (Liskov Substitution Principle)
LSP原則意指任何父類出現的地方都可以使用子類來代替,并不會導致使用相應類的程序出現錯誤。由於第二單元的三次作業我都沒有使用繼承,因此此原則沒有涉及。
2.4 ISP (Interface Segregation Principle)
ISP原則意指一個接口只封裝一組高度内聚的操作。由於第二單元的三次作業我都沒有使用接口,因此此原則沒有涉及。
2.5 DIP (Dependency Inversion Principle)
DIP原則意指高層模塊不應該依賴於底層模塊,兩者都應該依賴於其抽象。由於第二單元的三次作業我都沒有使用繼承和接口,因此此原則沒有涉及。
三、程序結構分析
- 首先是UML協作圖
因爲我沒有設計調度器進程,因此這個圖對於三次作業來説是通用的。主綫程將請求送到請求隊列,電梯綫程將請求取出並執行,簡單又明確。
作業1
- 下圖爲代碼行數統計信息
作業1較爲簡單,所以代碼行數不多。
- 下圖爲UML類圖
每個類各司其職,類之間關係清晰明確,電梯自調度很適用於一臺電梯的情況。請求隊列内部采用ArrayList的數據結構,對請求的管理不是很方便。
- 下表爲類的度量結果
Elevator中的公共屬性是public final int UP = 1
和public final int DOWN = -1
,以兩個常數代表電梯運行的方向,且其互爲相反數,方便電梯反向時狀態值直接取反。
作業1僅一部電梯,較爲簡單,因此并沒有使用繼承。
- 下表爲方法的度量結果
Elevator類的arrive方法較爲複雜,原因是我在arrive方法中進行了需要開門的判斷與電梯門開關、人員入出的過程模擬。Elevator類的run方法也較爲複雜,原因是我在run方法中進行了電梯宏觀運行狀態的判斷和模擬。
RequestQueue類中的hasRequestFrom、hasRequestTo、getRequestFrom、getRequestTo四個方法複雜度較高,原因是我在其中傳入了電梯運行方向,并依據電梯運行方向判斷該方向上是否有請求,代碼中有較多分支條件且各分支條件存在一定重複性,因此複雜度較高。
作業2
- 下圖爲代碼行數統計信息
比起作業1,題目需要更多電梯,需要對主類代碼進行修改。此外,我還將請求隊列由全部請求聚集在一起的ArrayList改爲了依據樓層對請求分類的HashMap,並改寫了相應方法,這也增加了部分代碼。還有,我將輸入綫程與輸出綫程共享的數據放到了一個單例類Dispatcher中,將這些數據集中管理,增加了代碼,使得整個程序結構更加清晰、明確。
- 下圖爲UML類圖
我認爲我的作業2的整體結構也還是比較清晰的。比起作業1,本次作業新增了一個單例類來對輸入綫程與電梯綫程之間的共享數據進行統一管理,這是一個進步。
- 下表爲類的度量結果
比起作業1,作業2的Elevator類新增了一個public final int MAXPSGER = 7
,代表了電梯的最大容量。作業2五個電梯各參數均相同,因此仍然不需要使用繼承。
- 下表爲方法的度量結果
複雜度高的方法仍然是作業1中複雜度就很高的方法,這與作業2中的邏輯比作業1中更爲複雜有關;但同時也因爲作業2的邏輯還不是那麽複雜,所以就沒有去主動降低這些方法的複雜度。
作業3
- 下圖爲代碼行數統計信息
代碼量直綫上升,因爲作業3確實很複雜。
- 下圖爲UML類圖
由於是直接在作業2的基礎上進行的迭代、而沒有進行重構,所以感覺很多地方都是在打補丁。新添加的類全部由靜態屬性和靜態方法構成、是爲了完成作業3中的特殊需求而生的、是隨時需要而隨時創建的,而不是經過深思熟慮得到的結果。我想這樣的代碼在本次作業還可以使用,但如果在作業3的基礎上還需要繼續迭代就比較麻煩了。
- 下表爲類的度量結果
本次作業是可以采用繼承的。但是我沒有使用,原因是:本次作業是在第二次作業的基礎上直接修改的,因此沒有對架構進行大變動。不同類型的電梯的不同特性體現在構造函數中對這些屬性的初始化,這使得構造函數有些臃腫。這也是爲了不對架構進行大改的權宜之策。
- 下表爲方法的度量結果
作業3對方法的複雜度進行了優化:將邏輯複雜的方法拆分成獨立的且有意義的獨立方法、將複雜的布爾表達式封裝到一個函數中。getTargetFloor的三個方法複雜度仍然較高,原因是其對換乘樓層進行了類似於枚舉的返回值。
四、bug分析
本單元作業很幸運,三次作業均無未通過的公測樣例和互測樣例,因此本節內容爲null。
五、發現他人bug策略
本單元作業1和作業3互測階段我均未發現他人的bug。作業2互測階段隨意提交的一組數據hack到了一名同學的死鎖bug,但是在本地無法復現。
事實就是我沒有什麽策略來進行測試,而我的三次作業的互測房間中同學們的bug真的很少,硬要説策略的話,就是瞎貓碰死耗子策略。
所以我認爲,本單元的測試策略與第一單元測試策略的差異之處就在於,本單元的測試策略玄學了很多。據説有同學想要hack一位同學的bug,結束之後才發現想hack的同學沒被hack,卻奇妙地hack到了其他同學。而我hack到的bug在本地根本無法復現。第一單元作業的測試可以有確定的結果,可本單元作業的測試卻真的有很多不確定因素,我想這大概就是差異吧。
六、心得體會
6.1 綫程安全
在多綫程程序設計中,綫程安全無疑是很重要的。我在這三次作業中均未遇到綫程安全問題,但這不意味著綫程安全問題就不存在了,只是我碰巧避開了而已。
據我的觀察,似乎同學們的綫程安全問題可能主要發生在在wait和notify上。而我恰好在這三次作業中都沒有用到wait和notify,這確實讓我避免了很多潛在的問題。但是,wait和notify並不是綫程安全的全部。我想,不是wait和notify造成了綫程安全問題,一切綫程安全問題都是由於對多綫程的理解不夠深入造成的。
個人認爲,在使用synchronized進行同步的時候,一定要想清楚一個問題:我鎖住的是誰?對於synchronized修飾的方法,如果是非靜態方法,鎖住的是this
;如果是靜態方法,鎖住的是class
。對於synchronized語句塊,鎖住的就是括號内的對象。我個人非常不建議直接在方法上使用synchronized修飾,哪怕我需要在方法的第一行就寫上synchronized (this)
或synchronized (class)
,因爲我認爲直接在方法上進行修飾的做法沒有明確指出鎖住的對象,非常容易引起混淆。而synchronized (...)
這樣的寫法靈活性會更高,可以對更多的對象進行上鎖。
此外真的非常推薦使用ReentrantLock,真的非常好用,也非常不容易產生混淆或迷惑。
6.2 設計原則
對於單一職責原則,我很努力地在去做。感覺大部分類確實也都是單一職責的,如果想要把這些類再細分我也真説不好能怎麽分。也不能爲了所謂的“單一職責”而將每個職責無限細分,這樣也不是一件好事。
對於開閉原則,我的感覺是:如果在上一次作業的基礎上擴展的話,一點都不去修改已有實現也確實挺困難的,感覺也不太現實。可能這個原則不是一個離散的評判標準,不能説做到就是1,做不到就是0,而應該采用“我做到了0.7的開閉原則”這樣的説法。我認爲這樣還是比較合適的。
對於其他原則,本次作業不涉及,因此確實沒有在作業中獲得到關於這些原則的體會。希望之後可以漸漸對這些原則都有所體會吧。
七、寫在最後
這是我第一次接觸多綫程程序設計,在此之前對多綫程從未有過相關的瞭解。而就在這一個月不到的時間裏,經過OO這一單元的學習、以及OS最近在講的進程管理内容的學習,我對多綫程程序設計這一方面的收穫巨大。本單元三次作業的得分也都比較令我滿意,也請容我內心小竊喜一下。希望OO接下來的兩個單元作業我都能順利完成,爭取在學到知識的同時,也能保持好前幾次作業已經取得的成績。加油!