1. 程式人生 > >OO第5~7次作業總結

OO第5~7次作業總結

進一步 標記 -a 使用 tail 文件信息 文件系統 設計框架 一半

作業5——多線程電梯

設計框架——UML協作時序圖

(想看大圖的話 crtl+滾輪 比較方便)

技術分享圖片

我為什麽不用UML協作圖(Communication Diagram),而是用UML時序圖(Sequence Diagram)。

一方面,這樣橫平豎直的圖更加直觀美觀。另一方面,使用Eclipse的plantUML插件能夠比較方便地生成時序圖。雖然從二者的區別上看:

時序圖主要側重於對象間消息傳遞在時間上的先後關系,
而協作圖表達對象間的交互過程及對象間的關聯關系。

協作圖似乎能夠更好地描述線程之間的協作關系,但是我發現有些軟件是可以自動由時序圖生成協作圖的,這說明二者本質上描述的東西是一樣的,只不過傳達的角度有所側重。相信聰明的你一定能夠多角度看問題嘿嘿嘿~

【插播一則 eclipse的plantUML插件安裝和使用方法】—— 摘自 http://www.importnew.com/24706.html

1. 安裝:

PlantUML for Eclipse 插件主要用於在 Eclipse 中使用 PlantUML。在 Eclipse 的插件市場中安裝,點擊 Help --> Install new software --> add --> 起個帥氣的name,location中輸入網址:http://plantuml.sourceforge.net/updatesitejuno/ --> 其它安裝操作

2. 點擊“Window/Show View/Other..”,可以將 PlantUML 預覽窗口面板顯示出來。

然後我自己摸索了下就這樣做了:在eclipse中右鍵工程--> New --> Untitled Text File (就是在工程文件夾下建了一個txt)。

在txt中按照一定的語法編寫代碼就能夠在PlantUML 預覽窗口中實時看到對應的時序圖。像這樣:

技術分享圖片

3. 所以編寫代碼的語法是什麽呢?超簡單的!

官方教程文檔(中文):

http://translate.plantuml.com/zh/PlantUML_Language_Reference_Guide_ZH.pdf

【插播完畢,回歸正題】

我的多線程電梯UML時序圖:

技術分享圖片

線程總數:主線程Main + 輸入處理InputHandler + 主調度器MainScheduler + 從調度器SubScheduler * 3 = 6個線程

(註:一共4個請求隊列,一個大的,3個小的分別是歸屬於3部電梯的)

  • 主線程:啟動其它線程。
  • 輸入處理器:不斷接受控制臺輸入,並進行分析,如果有效,則將請求放入請求隊列。
  • 主調度器:不斷掃描請求隊列中的請求。判斷是否是同質請求,是則刪除,不是則:判斷是否能被某部電梯捎帶,是則選擇一部可捎帶電梯並將這個請求丟給它,不是則:找出運動量最小的電梯並將這個燙手請求丟給它。
  • 從調度器:不斷檢查自己所管轄的電梯的小隊列的情況以及電梯的情況,來決定並通知電梯接下來的動作等。

設計類圖

有了設計框架,就進一步細化成 設計類圖:(我手動進行了刪減和移位)

技術分享圖片

程序度量分析——Metrics

技術分享圖片

圈復雜度大在哪?

技術分享圖片

嵌套塊深度大在哪?

技術分享圖片

不出所料...當初寫這個作業的時候沒有想那麽多設計上的權衡之類的問題,只是一股腦地想做出來...所以該包裝的、該劃分功能的地方我都有所忽略。

然後打開代碼,就發現以前怎麽這麽醜陋啊啊啊

  • 主調度器的輪廓是這樣的:

技術分享圖片

  • 從調度器和輸入處理器比它好一些。電梯中也有些小方法是 > 形狀的...

線程安全的考慮

  • 雖然3個小請求隊列 是3個電梯獨有的,只會被從調度器使用,但是 大請求隊列是 輸入處理器線程 和 主調度器線程 共享的對象,因此我將請求隊列類的所有 修改或者讀取隊列內容的方法 都設置成同步方法。(這麽一想,是不是電梯的隊列 應該另做一個不需要同步的隊列類 來提高一絲絲效率呢?)
  • 3個電梯是 主調度器 和 從調度器 共享的對象,因此我將電梯類的所有 修改或者讀取電梯信息 的方法 都設置成同步方法。
  • 當時比較年輕的我還是不大放心,又在4個調度器的run方法裏面的某些代碼塊進行了同步化。

自評優缺點和反思設計原則

  • 優點:不是無效。總體框架還算清晰有邏輯。
  • 缺點:(1)細節的實現沒有考慮簡化,只是想做出來而已...所以搞得有些復雜,導致心情浮躁課下漏洞較多。(2)主調度器做的事情有點多,應該把判斷同質請求放在到 輸入處理器 或者 請求隊列 中。(3)沒有把功能進行 劃分、精細化,導致幾個很大塊的方法的存在,可讀性、可維護性不強。
  • 反思設計原則:(1)最大的問題:方法的職責不夠單一、不夠精細,也就是違反了SRP原則、責任均衡分配原則。(2)沒有使用枚舉類、全局靜態變量等,不符合顯式表達原則。(3)高層次的模塊直接依賴於低層次的模塊而非抽象,即沒有考慮到DIP原則。

Bug分析

  • 課下調試:(1)沒有使用枚舉類 而且在判斷字符串是否相同的時候 直接使用了==,我真的是=.=,而且是多處如此,而且其中有一個地方忘了改過來導致沒有過公測的那個權重為5的測試點真是心痛心痛心痛。(2)在主請求 從無到有、從有到無、由一個變到另外一個即升級未完成的捎帶請求為主請求 這幾種變化情況上,急匆匆地就搞得比較復雜,出現了一些bug,然後我靜下心來重新捋一遍就好了。
  • 公測:沒有過公測的那個權重為5的測試點真是好心痛。
  • 互測:我方安全,對方完美。
  • 我栽大了。
  • 順帶感謝一波laj小朋友對我的信任!


作業6——IFTTT監控程序

設計框架——UML時序圖

技術分享圖片

技術分享圖片

  • 為什麽沒有輸入處理器:首先,你要知道,我的程序的輸入順序是醬的:
  1. 控制臺接受IFTTT指令,直到輸入RUN,表明指令輸入結束。
  2. 根據輸入的指令,啟動所有的相應的觸發器線程,讓主線程睡眠一小段時間,以便於初始化所有觸發器線程和快照。
  3. 創建和啟動測試線程,進行一系列文件操作,來進行測試。
  4. 在啟動測試線程之後,主程序將等待您在控制臺輸入END來結束所有還未結束的觸發器線程。
  • 可見,輸入處理器並不是一個單獨的線程,所以我就懶了一把,直接把輸入處理的方法都寫在Main的類方法中了。
  • 上圖中,為了簡化,就用父類Monitor 來代替 4種監控器線程,因為每類監控器做的事情類似:不斷獲取新的快照,並與舊快照相比,來判斷是否滿足觸發條件並執行相應動作,以及是否滿足監控器的結束條件。每類監控器的屬性以及做的事情中的相同的部分,我都提取到了父類Monitor中。
  • 一共有4類監控器線程,每個監控器線程以 被監控對象(某個目錄或者某個文件)和 觸發條件即監控器類型(4選1)作為 這個監控器線程區別於其他監控器線程的標誌,也就是說 本程序的監控器線程最多有10*4個,而非10*4*3個。

觸發條件和監控條件經過我的readme如下:(正如我上一篇博客所提到的策略,我進行了所有情況的遍歷,以避免我遺漏了什麽條件)

技術分享圖片

表1

技術分享圖片

表2

設計類圖

再細化一下,就有了設計類圖:(我手動進行了刪減和移位)

技術分享圖片

程序度量分析——Metrics

技術分享圖片

圈復雜度又是哪裏大?

技術分享圖片

嵌套塊深度又是哪裏大?

技術分享圖片

  • 嗯,還是老問題,沒有把功能進行細分包裝。
  • 而且我的四個類別的監控器的run方法比較相似:我是先寫完一個最難的RenamedMonitor,然後把它的bug找得差不多了,才復制到其它3類監控器中,進行部分代碼的修改,這樣能夠盡量避免 在發現一個bug之後,不會手忙腳亂地去改4個或者2個子監控器類的代碼。

  • 【分享我這次的碼代碼經驗】
  • 首先搭了一個差不多的類的框架,然後寫了Main方法以及輸入處理,並進行相應的debug
  • 然後寫完一個最難的RenamedMonitor
  • 然後在RenamedMonitor中開一個主方法,在這個主方法中通過參數構造一個RenamedMonitor對象,然後調用他的 run( )方法
  • 開始單步調試,然後直接操作文件系統:進行創建、移動、重命名等操作,文件操作單步調試 交叉進行,同時你還可以查看你實時輸出的detail或者summary內容。
  • RenamedMonitor監控器的bug找得差不多了。同理可以單獨調試另外3個監控器類。
  • 最後單線程調得差不多了,就開啟多個線程,這時,就不能像上述那樣 在多線程的邊緣試探 了....老老實實打印出來debug吧,這個時候出現的問題,基本就是線程安全和協作的問題了。前面的步驟已經為你de掉了許多小bug還是可取的。

線程安全的考慮

  • 拍快照的時候,會讀取文件信息,要求此時文件的信息不能發生改變,因此將 線程安全的文件類SafeFile 的方法設置成了同步方法。
  • 另外,如果僅僅按照上一條所述進行同步,仍然可能出現這種情況:遍歷某個文件夾下的文件進行拍照時,有可能拍照拍到一半的時候,CPU切換給了測試線程,測試線程對文件A進行了某些操作,當CPU切換回來繼續拍照時,照相機可能已經對文件A拍過照了,這時就有會把 A文件被更改這個消息 延遲到 下一次拍快照的時候 發現。
  • 為了解決這個問題,我想加把大大的鎖,進行文件操作或者拍快照之前,都必須獲得這把大鎖,這把全局鎖是這樣的:Object lock = new Object( )。
  • (但是現在想想,好像這個延遲的問題並不大,反正馬上就要進行下一次拍照了QAQ....)

自評優缺點和反思設計原則

  • 優點:總體框架比較清晰有邏輯;自己想出來了個在多線程邊緣試探的debug方法;把 四個類別的監控器 的共同方法 和屬性 抽出來放到了父類Monitor裏面,代碼重用性還算好。
  • 缺點:(1)有些設計沒有必要...(2)沒有把功能進行 劃分、精細化,可讀性、可維護性不強。
  • 反思設計原則:(1)方法的職責不夠單一、不夠精細,違反了SRP原則、責任均衡分配原則。(2)沒有考慮到DIP原則。

Bug分析

  • 課下調試:對象復制時,什麽時候是要 對對象的引用進行復制,什麽時候是要 clone一個新的對象出來,這個問題在我調試時反復折磨我,於是我趕緊搞清了這二者的區別,然後又從頭縷了一遍。
  • 公測:安全。
  • 互測——對方:對方的設計思路是最多開120個監控器線程的那種,由於處理不當出現了 “若監控一個文件,且被觸發後要執行的任務多於1個,若該文件滿足了觸發條件,那麽有時候這些任務都會被執行,有時候這些任務只會被執行一部分”,這是設計框架的時候容易忽略的一個問題。(我當初沒有想過這個問題,當初選擇 最多開40個監控器線程的思路 的時候,純粹是怕120個線程太多了....)
  • 互測——我方:我方拋出一個NoPointerException異常,似乎是因為我後來為了能實現 “若監控目錄,且滿足觸發條件後的任務包括recover,那麽每當監測到一個文件滿足觸發條件後,就把快照回退到這個文件改變前的狀態” 這一目標,而導致我解決了一個bug後另一個bug又浮出水面....
  • 我服。


作業7——出租車的乘客呼叫與應答系統

設計框架——UML時序圖

技術分享圖片

技術分享圖片

線程總數:主線程TaxiSys + 輸入處理InputHandler + 調度器Scheduler + 圖形化界面GUI + 出租車Car * 100 = 104個線程

  • 主線程:啟動其它線程。
  • 輸入處理器:不斷接受控制臺輸入,並進行分析,如果有效且非同質,則將請求放入請求隊列。
  • 調度器:不斷掃描請求隊列中的請求。若請求窗口時間未到,則在所有出租車中尋找具備搶單條件的車。若請求窗口時間到了,則從隊列中刪除該請求、從所有搶單的車中篩選出最終進行派單的車,然後跟這輛車說,你去響應這個請求吧。
  • 出租車線程:不斷檢查自己的狀態等信息,根據自身信息來決定自己接下來該做什麽。
  • GUI:沒什麽好說的。

設計類圖

有了設計框架,就進一步細化成 設計類圖:(我手動進行了刪減,為了提取重點和優美)

技術分享圖片

再細化一些,我的程序包括這麽些類:

技術分享圖片

  • 打黑色圓圈的是上述104個線程。
  • 出租車的屬性比較多,而且有些是不需要被外人知道的,為了傳遞出租車的信息時更加簡便,我就做了個出租車信息的類。(我為什麽沒有做RequestInfo,因為請求的信息比較簡單...純屬個人想法,如果您覺得這樣做不大對,歡迎留言討論)
  • 為了抽象,我抽取出汽車的運動方法成CarMove接口。(或許以後滴滴還能有truck、motorbike...)
  • ErrorHandler你懂的。
  • FileLogger是輸出日誌的類,考慮到DIP原則和擴展性,我又把它抽取成Logger接口。
  • gui是課程組提供的,為了讀入地圖文件信息並轉化為數組,我又做了MapInfo。
  • 做MyPoint類是為了擴展Point類的功能。
  • 請求和請求隊列你也懂的。
  • 還有一些表示 狀態類型、錯誤類型的 枚舉類,就沒有列出了。

程序度量分析——Metrics

技術分享圖片

與前兩次相比有了較大進步。

圈復雜度哪裏大?

技術分享圖片

嵌套塊深度哪裏大?

技術分享圖片

  • gui類我沒辦法了肝不動了....
  • InputHandler和MapInfo都是輸入處理,我就沒考慮拆分。我的輸入處理比較精確,寫了35行代碼,看來要拆分一下。
  • Scheduler我拆分包裝得挺好了呀,咋回事?找到Scheduler的run方法,我發現了這個東西 :)

技術分享圖片

線程安全的考慮

  • 請求隊列是 輸入處理器線程 和 調度器線程 共享的對象,所以我將請求隊列的所有 修改或者讀取隊列內容的方法 都設置成同步方法。
  • 每個出租車對象是 這個出租車線程 和 調度器線程 共享的對象, 所以我將出租車的 修改或者讀取 出租車狀態等信息 的方法 都設置成同步方法。

自評優缺點和反思設計原則

  • 優點:結構和邏輯比較清晰,因此debug比較方便。
  • 缺點:感覺不是很工程化;在給某一個請求尋找能夠搶單的出租車時,采用的是遍歷100輛出租車的方法,這樣似乎有點浪費資源消耗時間。或許可以如老師所說,【給出租車做幾個隊列,不同狀態的車進不同的隊列】,這樣查找起來比較方便,又或者,【給出租車做幾個隊列,不同區域的車進不同的隊列】?
  • 反思設計原則:這次作業前老師講了設計原則,因此我這些設計原則實現得還算不錯,不過都是自我感覺良好....還是要去看看真正的工程化代碼[fighting]

Bug分析

  • 課下自我調試:(1)曾經在使用 wait(200) 還是 Thread.sleep(200) 這件事上猶豫了,最後我是這樣考慮的:我的出租車類的方法基本都是同步方法,這輛出租車自己 和 調度器是 唯二的 可能拿到這輛出租車的鎖 的人,出租車在運動過程中花費200ms的時候,應該把鎖讓出去給調度器,所以用wait(200)。當然如果你的線程同步設計和我不同,可能結論也不同。(2)為了能夠實時輸出日誌信息,且為了避免所有請求實時輸出到一個文件時發生的交錯 不優美 的問題,我給不同的請求都分配了日誌,但是課下debug時,居然出現了 請求1的部分日誌信息寫到了 請求2的日誌文件中去 的魑魅魍魎現象,我百思不得其解,後來不知道為什麽,又好了....(有遇到同樣問題且知道幕後黑手的同學歡迎留言...)
  • 公測:通過
  • 互測:我方安全,對方的輸出不符合指導書和issue的要求,而且請求多於3個就拋異常,而且還有一輛車同時匹配2個請求的情況。
  • 終於平安耶。


其它總結

設計策略的變化

  1. 心態上的變化:一開始接觸多線程的時候,如上述所講,我比較急匆匆、暈乎乎,後來掌握了一些套路,就能夠冷靜下來分析,特別是線程安全相關的問題。當然,冷靜的同時還要有緊迫感,反正我隔三差五跟自己說:你要無效了無效了笑了...
  2. 由全局到細節地考慮。我一般是這樣的,先考慮和討論大框架:應該有多少線程、分別都做什麽事情、有共享什麽對象嗎,這樣的設計能夠滿足那些硬性的大的需求嗎。然後再去考慮如何調整小部件去實現那些小的需求。然後感覺差不多了,我就把大概想一想 列一列 有什麽類、大概有什麽屬性和方法、有共同點可以提取嗎、有什麽線程安全問題要解決。然後開始在紙上把 盡量考慮到各個設計和實現細節的偽代碼 寫出來。然後開始碼代碼,在碼代碼的過程中可能會發現自己偽代碼的一些錯誤或者遺漏,這個時候就能補救;另外,每寫完一個 可以測試的 適度規模的 模塊的時候,我就先對這個模塊進行debug。最後再對整個多線程程序以打印的方式進行debug。
  3. 盡量不要推倒重構(反正我是沒有這個能力和勇氣)。這就是我為什麽采取上述2策略的原因。
  4. 碼代碼的時候就盡量考慮到功能的單一化精細化,不要相信 你有時間 能夠在de完bug之後 把某些代碼段提取出來成方法 進行功能的歸一化。

發現bug的策略

在我的上一篇博客的所述策略——

  1. 解剖指導書。我遍歷了幾遍指導書,而且自己用筆把一條條規定都簡單列了出來,用紅筆標記不同條列之間的關系以及易忽略的點。然後進行一些深入思考,比如指導書說到出租車可能的幾個狀態的時候,我就畫了個狀態機出來。深入了解指導書是前提。
  2. 給自己程序打過的補丁,也可能是別人容易漏掉的。
  3. 不同的設計方式會有不同的易錯點,如果你抽到的代碼的設計方式和你不同,可以找找其他使用這個設計方法的同學,問問他們課下de出來了什麽bug。比如IFTTT作業中的“最多40個線程設計”和“最多120個線程設計”,這就屬於兩種不同的設計方式。
  4. 討論區和微信群裏大家的討論也都一條條列出來。
  5. 與你親愛的同學交換測試樣例。(多線程的話,這條基本不管用了....)
  6. 以更加宏觀、復雜、遍歷的視角去看待問題,就更容易發現問題所在。比如我在IFTTT作業中,將不同文件操作是否滿足4種觸發器的觸發條件或者結束條件 列了個表格分析。

的基礎上,又總結出了一些策略:

  1. 由簡單到復雜地進行自測。自我測試的時候好理解,從小補丁開始打起。
  2. 互測的時候,你就可以靈活些了。你可以一上來先炸它100個輸入(比如100個出租車請求),看他是不是大佬,如果結果和預期不同,你就可以輕輕一笑(如果結果和預期相同,你還可以祈禱它有小bug),然後用 分離的 功能點的 測試樣例 去測他的代碼,然後一步步增加測試樣例的復雜度。(好吧,我承認一上來炸它一個大數據主要是為了把握大局....)
  3. 靈活地構造測試樣例。比如出租車作業中,我構造了這麽一個好玩的樣例:在 全世界的每個小區域裏 發出乘車請求,目標北航,然後看著GUI上出租車大部分都到了北航的時候,乘客們發現北航人都開飛機去了,於是乘客們在北航附近 發出了許多乘車請求想要離開,去哪裏就隨意吧。這個時候,會出現許多輛出租車搶單的情況,然後你就可以根據 日誌輸出的搶單的車在請求窗口結束時的信息 來判斷應該是哪一個輛車最終被分配到請求。(好吧,這個樣例只是好玩而已,可能並不是很有效嘻嘻....)
  4. 我一般不會仔細地去看人家的代碼,除非那人代碼特別棒沒有bug我就會開始欣賞。但我會大概結合被測程序的代碼設計結構,來設計測試用例。因為不同的設計結構會有不同的容易遺漏的問題。

        再次感謝各位幫助過我的大佬麽麽噠~

也繼續預祝大家 習得OO,策馬歸來~          

OO第5~7次作業總結