2019年北航OO第2單元(電梯模擬)總結

来源:https://www.cnblogs.com/mhzhang/archive/2019/04/23/10759230.html
-Advertisement-
Play Games

1 三次作業的設計策略 經過了上一單元的訓練,我也積累了一些設計策略上的經驗。在這一單元的一開始,我便儘可能地把問題中的各個功能實體區分開來,分別封裝成類,以便於隨後作業中新需求的加入。與此同時,我也在有意地控制住方法的規模,依照程式邏輯層次化地設計方法,使得每個方法都不至於過分臃腫,從而增加代碼的 ...


1 三次作業的設計策略

經過了上一單元的訓練,我也積累了一些設計策略上的經驗。在這一單元的一開始,我便儘可能地把問題中的各個功能實體區分開來,分別封裝成類,以便於隨後作業中新需求的加入。與此同時,我也在有意地控制住方法的規模,依照程式邏輯層次化地設計方法,使得每個方法都不至於過分臃腫,從而增加代碼的可重覆利用性,減輕編程負擔。

接下來,具體介紹每次作業的設計策略及其演進。

1.1 第1次作業

第一次作業的需求較為簡單,只需實現單電梯先來先服務演算法的調度模擬即可。為了儘可能模擬出電梯運行的真實行為,我採用了雙線程設計,電梯和輸入分別為兩個獨立運行的線程。其中,輸入線程為main線程,最先開始最後結束。在當時這樣做的目的是為接下來可能的多電梯系統作准備。

類的設計需要依照問題中的實體。本次作業中的實體包括電梯、調度器和乘客,由於乘客的全部信息可以由課程組提供的PersonRequest類完全表示,因此只需封裝前兩個類即可,同時也可以保障程式的可擴展性以及模塊化設計。

調度器類(Controller)是一個單例的類(single-instance class),即這個類在程式中只允許被實例化一次。本次作業里Controller的內容很少,成員變數只有一個隊列,方法也只有“出隊”、“入隊”和“判斷隊是否為空”3個。就這一次作業來說,這個類更像是為了儲存和維護乘客隊列而封裝的,並沒有涉及到“調度”的操作,至於FCFS演算法的具體實現我放在了電梯類裡面。

這裡需要特別指出的是,另一種可能的設計方式是調度相關的代碼完全放在Controller裡面,即一切有關“調度”的內容全部在調度器里,電梯只需“指哪去哪”,每一個動作都由調度器來命令。這樣做似乎是實現了功能的分離和耦合度的降低,但是在我看來,這種設計的代價是調度器里的調度方法會極為複雜,因為它需要將乘客的請求按照具體演算法進行順序重排,還要進一步拆解為電梯的執行動作。這就好像有兩個聰明的工人,明明可以讓他們各自分擔一部分“思考”的任務,通過交流共同完成一個複雜的任務,卻偏偏只讓其中一個人承擔全部的“思考”任務,把另一個人當成傻瓜並一步一步教他需要怎麼乾。

按照上述的設計思想,電梯類(Elevator) 會略微複雜一些。成員變數包括電梯的當前狀態,另外還需要一個變數儲存調度器類的一個實例(事實上調度器也只有一個實例)。方法包括基本動作(如開門、進1個乘客等),以及由這些基本動作組成的FCFS演算法實現。另外一個值得單獨說明的方法是停止方法end()。這個方法雖然在Elevator類里,但是並不在電梯線程被調用,而是在main線程(即輸入線程)里通過Elevator.end()的方式被調用。該方法首先判斷當前程式狀態是否滿足電梯進程結束條件,若不滿足則進入阻塞狀態,直到條件滿足,結束電梯進程。

程式的整體運行流程:main線程首先實例化相關的類,開啟電梯線程;隨後開始掃描輸入,逐個加入隊列;當輸入掃描完畢後開始嘗試停止電梯線程,成功停止掉之後結束整個程式。

1.2 第2次作業

第2次作業相比第一次作業的不同之處在於調度演算法從先來先服務(FCFS)改成了可捎帶調度(ALS)。因此程式的大部分內容都不需要改動,只需重寫電梯類里調度方法,以及在調度器類裡面增加相應的方法即可。具體地,電梯類需要維護一個乘客集合,儲存當前在電梯里的乘客。第1次作業中同一時刻電梯里至多只有一個人,所以這樣的集合完全沒有必要。調度器類新增的方法是“查詢是否有可捎帶的乘客”,輸入參數包括當前運行方向和當前層。相應地,需要增加新的枚舉類Direction,內含三種方向:UP, DOWN, STOP。

這裡就可以初步看出功能平攤帶來的好處。調度器類提供一個查詢捎帶的介面,而電梯類只需把查詢操作集成到調度里即可。如此,將一件較為複雜的工作拆分成兩個部分,由兩個類分別完成,在保持低耦合度的同時降低了每部分工作的邏輯複雜度。

1.3 第3次作業

第3次作業相比於上一次作業最核心的變化在於電梯數量增加到了3部,而且每部電梯並不是等價的,擁有不同的運行速度、不同的轎箱容量、不同的停靠樓層。由此帶來一系列新的問題需要重新思考和設計,包括乘客換乘和多電梯調度等等。總的原則是:能用一部電梯完成的任務絕不用多部電梯完成。下麵我來分別就各個方面進行介紹。

首先,需要新增一個Person類,儲存原先PersonRequest類里的所有信息,外加當前目的層nextDest以及電梯選擇choices。nextDest的加入是因為換乘問題,用來儲存“該乘客在當前這部電梯里應該在哪一層下電梯”,若nextDest和toFloor相等則說明該乘客從今往後不用再進行換乘了。choices的加入則是因為電梯停靠層各不相同,用來儲存該乘客當前可以選擇的電梯。這兩個變數會在新乘客加入到乘客隊列前由Controller類初始化。

此外,Controller類採用了訂閱模式,每個電梯線程在開始時都會向Controller註冊。新提供的調度相關的查詢介面為“為乘客分配換乘層”(換乘層集合根據所有註冊電梯的停靠樓層動態更新)。

Elevator類的改動雖然較多,但大都屬於適配工作。值得一提的是換乘問題的處理:電梯每次只關心乘客的當前目的層,而只有在乘客下電梯時才檢查當前目的層和最終目的層是否相等,若不等則改變出發層fromFloor並將該乘客重新加入到等待隊列中。這樣做相當於把乘客的旅程劃分為一段段更短的、可由一部電梯完成的旅程。每個電梯的調度演算法仍以捎帶演算法為基礎。

2 基於度量的程式結構分析

2.1 量化指標及分析

以下是三次作業的量化指標統計:


關於圖中指標在上一篇博文中提到過,不過我覺得有必要再在這裡回顧一下:

  • ev(G):基本複雜度,用來衡量程式非結構化程度。基本複雜度高意味著非結構化程度高,難以模塊化和維護。
  • Iv(G):模塊設計複雜度,用來衡量模塊判定結構,即模塊和其他模塊的調用關係。模塊設計複雜度高意味模塊耦合度高,這將導致模塊難於隔離、維護和復用。
  • v(G):模塊判定結構複雜度,數量上表現為獨立路徑的條數。

從上面三張圖可以看出,整體上3個複雜度指標在這一單元的三次作業中都維持在一個較低的水平,僅有個別的複雜方法的某些指標超出了正常值。事實上,和我上一單元寫的程式相比,我認為能看出非常明顯的進步。另外,異常值超出正常範圍的程度也比上一單元好了很多,上一單元有些方法的iv(G)和v(G)超過了15甚至20。具體來說,問題大致集中在第3次作業的Elevator.operate()方法上,而這個方法正是我實現調度演算法的地方。雖然我已經有意識地將基本操作獨立封裝,儘可能降低邏輯結構的複雜度,但是還是未能將其限制在正常的範圍內。我想原因是多方面的,可能是演算法本身的複雜性導致的,也可能是我的設計存在失誤,這一部分也是我在下一單元需要著重解決的問題。不過總體上,我認為這一單元我的程式設計架構在控制耦合度、非結構化程度等方面還是有了很大改善。

2.2 類圖及分析

三次作業的類圖如下:


從類圖的變化可以明顯看出,隨著作業進度的推進,類的內容越來越豐富,類的結構越來越複雜。這樣的變化也是和日益增加的需求相一致的。不過我認為我在這一單元做的還不錯的地方在於,從整體的角度上來看,隨著作業進度的推進類之間的聯繫並沒有增加多少,這說明這個程式的功能雖然增加了,但是耦合度並沒有顯著上升。

2.3 線程圖及分析

第1、2次作業的線程圖完全一致,如下麵左圖所示,第3次作業的線程圖如下麵右圖所示:

從圖上可以看出這一單元的作業雖然是多線程程式訓練,但其實線程結構還是極其簡單的。三次作業都是從主線程出發,根據電梯數量的不同經由Elevator類開啟多個線程,最後線程回到主線程結束整個程式的運行。

2.4 按SOLID設計原則重新審視

先介紹一下什麼是SOLID設計原則:

  • Single Responsibility Principle,每個類或方法都只有一個明確的指責
  • Open Close Principle:無需修改已有實現,而是通過新增加代碼來擴展新的功能
  • Liskov Substitution Principle:任何父類出現的地方都可以使用子類來代替,並不會導致使用相應類的程式出現錯誤
  • Interface Segregation Principle:用介面類來規範程式的行為,劃分程式的功能
  • Dependency Inversion Principle:反轉傳統的依賴關係,高層次的模塊不依賴於低層次的模塊的實現細節

對於我自己的程式,L和I原則並未涉及,S原則O原則我認為我比較好地遵守了,而D原則並不是完全符合。思考其原因,我認為首先當前的程式還沒有複雜到有這樣做的必要性,當然不是說D原則並不重要,只是其重要性還沒有被很充分地體現出來。另外一個原因是這一單元需求的演進大多屬於增補型的變化而不是修改型的變化,也使得我某種程度上忽視了“尋求降低對低層次模塊實現細節的依賴”。這一點是我需要在今後的練習中改進的。

3 程式bug分析

需要提前指出的一點是,由於高工的OO課程並沒有互測環節,因此以下的分析都是基於我自己在調試過程中找到的bug以及未通過公測用例的情況。此外,這3次作業的所有強測測試點均為AC,所以也不在這裡討論了。

3.1 bug特征與位置

經過了上一單元的訓練,本單元的程式幾乎沒有Java語法以及對象相關的錯誤出現。大多數bug屬於程式邏輯錯誤或是程式運行到了未知狀態,歸根究底設計之初並沒有考慮所有可能的情況,這樣的錯誤相較於語法錯誤和對象使用錯誤更難被髮現。另外在多線程程式中調試工作遠比單線程程式複雜和繁瑣,導致定位bug也變成了一件有些挑戰的事情。

至於具體的bug特征,我想以上文提到的Elevator類里的end()方法為例,該方法判斷當前程式狀態是否滿足電梯進程結束條件並結束進程。代碼如下:

void end() {
    // stop the elevator only if:
    // 1. there are no passengers in this elevator
    // 2. there are no requests
    synchronized (passengers) {
        while (!passengers.isEmpty()) {
            try {
                passengers.wait();
            } catch (InterruptedException e) {}
        }
    }
    synchronized (controller) {
        while (!controller.isRequestsEmpty()) {
            try {
                controller.wait();
            } catch (InterruptedException e) {}
        }
    
        // woken up
        thread = null;
        controller.notifyAll();
    }
}

從代碼里可以看出,結束線程的條件有兩個:電梯里沒人,且電梯外沒人等電梯。這個方法裡面第1個易出bug的地方在於如何同時鎖住兩個共用對象。如果用兩層synchronized嵌套的話極容易出現死鎖。假設先鎖住passenger(電梯裡面的乘客集合),等待passenger為空(即電梯里的人都下電梯),然後鎖住controller等待controller的乘客隊列requests為空進而結束線程,但是乘客離開等候隊列的唯一方式就是上電梯(即進入passenger隊列),但是此時passenger對象已經被鎖住無法訪問,於是thread變數永遠不會等於null,線程永遠不會停止。因此我改成了上面代碼里的這種順序鎖的形式。當然你也許會問,最後一個乘客上電梯後thread = null;這條語句不就被立刻執行了嗎?是的,但是thread變數為空並不代表線程立刻結束,而是表示現在的電梯正在運送最後一個乘客。結合下麵run()方法的大體框架就會一目瞭然:

public void run() {
    while (thread == Thread.currentThread()) {
        ...
        ...
        operate(pass);
    }
    ...
    // here is the true end of this thread
}

我寫這個方法時遇到的第2個bug相對簡單,是忘記為passenger.wait()匹配相對應的passenger.notifyAll(),導致一旦該線程從這裡進入阻塞狀態就再也不會被喚醒。

從上面這個例子可以一窺多線程編程中bug的複雜性,一處錯誤可能波及到多個方法甚至多個類,這使得debug的難度陡然上升。

3.2 bug與設計結構之間的相關性

從以上對於bug位置和特征的總結來看,本單元出現的bug和設計結構之間的關聯更加緊密。

靜態地看,大多數情況下bug產生在邏輯複雜或是代碼量大的類和方法中。動態地看,從線程的角度,bug大多產生線上程“交匯”的地方,如開始、結束條件判斷和訪問共用對象時。如上一小節提到的,有時線程會早於或晚於我們所預期的時刻結束。所以為了在今後避免類似的bug出現,我們不能只在腦海中模擬程式運行的工作流(因為這樣做不穩妥,而且線上程更多的程式中根本不現實),而是要用各種輸出和log信息顯式地展示出各線程的運行情況,以保證設計邏輯的正確。

4 心得體會

經過這一單元的3次訓練,我自己認為已經對多線程編程有了初步的瞭解。具體來說,能夠準確識別出共用對象,並且能夠給共用對象的“敏感”操作加上合理粒度的鎖。

其實保障線程安全的難點在於架構設計而不是具體的代碼實現。基於一個好的的程式架構很容易就能定位到共用對象,識別出原子操作的範圍,進而對它們進行保護。那麼想得到好的程式架構就要在設計的時候遵循一定的原則,如上文提到的SOLID原則等。一方面,公認的一些設計原則有助於我們設計處更規整、更符合實際、耦合度更低的系統;另一方面,如果我們死板地恪守原則其實也不合理,每個程式都有不同的需求,針對具體問題也要有所變通。

我認為,對於線程安全這個問題,需求的分析和頂層的設計策略固然關鍵,然而更重要的是時刻緊繃一根弦,時刻擁有一種保護線程安全性的思想,(即老師課上所提到的,防禦性編程)。保護線程的安全是相對容易的(至少在Java語言中),難的是你能想到,而且每次都能想到需要保護它。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • <html><body> <h3>js控制文件上傳數量</h3> <form action="" enctype="multipart/form-data"> <input type="file" name="file" multiple="multiple" onchange="fileCount ...
  • 一、什麼是小程式? 基於微信的可以為用戶提供一些服務的web項目,利用微信提供的介面可以讓所有開發者使用到微信的原生能力,去完成一些之前做不到或者難以做到的事情。 二、小程式開發工具以及語言? 小程式需要用到微信提供的小程式開發工具,​小程式的主要開發語言是 JavaScript 。 三、小程式與普 ...
  • 項目中經常遇到要選擇城市。用到三級聯動的方式 微信小程式的 組件 是三級聯動的,但是無法自定義,這讓我們心痛不已,值得我們欣慰的 picker view 組件是可以自定義添加多個選項,但還是無法聯動。既然這樣那就自己寫一個聯動。 做到如下圖所示: 分為動態獲取地址 引用靜態文件獲取地址 <! mor ...
  • 轉自:https://www.cnblogs.com/kidsitcn/p/7182274.html 比例尺函數是這樣的javascript函數: 接收通常是數字,日期,類別等data輸入並且: 返回一個代表可視化元素的值,比如坐標,顏色,長度或者半徑等 比例尺通常用於變換(或者說映射)抽象的數據值 ...
  • 做法就是使用iframe標簽 1.text,pdf的文件預覽 <iframe class="filename" :src="文件的地址" width='100%' height='600' frameborder='1' ></iframe> 2.doc,xls,ppt等office的預覽 <ifr ...
  • 一、對象的擴展 1.1對象屬性名錶達式 ES6可以在JSON中使用[]包裹一個key的名字。此時這個key將用表達式作為屬性名(被當做變數求值),這個key值必須是字元串。 1.2 Object.assign()方法 該方法用於對象的合併,將源對象的所有可枚舉的屬性,複製到目標對象。 Object. ...
  • 譯者按: 為什麼偏要用 符號? 原文 : "JavaScript's new private class fields" 譯者 : "Fundebug" 本文采用意譯,版權歸原作者所有 "proposal class fields" 與 "proposal private methods" 定義了 ...
  • 文章首發: "結構型模式:組合模式" 七大結構型模式之三:組合模式。 簡介 姓名 :組合模式 英文名 :Composite Pattern 價值觀 :專門解決各種樹形疑難雜症 個人介紹 : Compose objects into tree structures to represent part ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...