天天看點

面向對象程式設計第二單元總結(電梯系列)

2019面向對象課程式設計第二單元總結

前言

​ 本次作業開始采用了多線程的設計模式。在之前的程式設計學習中,我們所編寫的程式都是單線程的,不需要考慮線程安全性的問題,而在這個單元,由于排程器和電梯需要同時運作并進行互動,即排程器需要給電梯派發請求,或者電梯從排程器中擷取請求,我們就需要建構多個線程來實作我們的需求。是以,我們就需要考慮線程安全和線程互動問題。例如,我的設計是排程器給電梯請求還是電梯從排程器中獲得請求?當暫時沒有請求,而兩個線程又不能退出,如果不采用輪詢的方法,我該怎麼辦?這裡不僅有着線程安全的問題,還有着一個新的概念——鎖,為了保證每一個對象的同步方法或同步語句塊隻有一個線程在通路,就需要鎖來進行維護。對于線程安全和鎖這個概念及其應用,還需要閱讀更多的資料去學習。

1.需求分析

​ 本次作業的總體目标是建構一個或多個載人電梯,電梯應當有合理的運作邏輯:必須開門才能上下人、關門以後不能上下人、必須嚴格按照乘客的需求上下人、不能跳層、不能超載,隻能在指定樓層停靠等。并且能在給定的時間内完成所有合法的乘客請求,即把所有乘客從指定起始樓層帶往終點樓層。

2.作業設計政策的分析與總結和程式結構分析

​ 這次作業的複雜度分析這部分我依然采用了IDEA的Metrics插件的Complexity mertics,對于這個插件有一些術語在這裡需要解釋一下:

  • ev(G): 一個方法或者類的結構化複雜度,值越高複雜度越高。
  • iv(G): 一個方法及其調用的其他方法的緊密程度,值越高則緊密程度越高。
  • v(G): 一個方法或類的循環複雜度,值越高則循環複雜度越高。在類中,有OCavg和WMC兩個名額,分别代表類的方法的平均循環複雜度和總循環複雜度。

    由于本單元中我沒有用到繼承和接口,是以在solid分析中将不考慮LSP,ISP,DIP這三個分項。

2.1 第五次作業(FAFS電梯)

2.1.1 設計政策

​ 第一次作業比較簡單,因為隻是采取了FIFO政策,一次隻帶一名乘客,是以隻需要設計一個排程器,排程器負責維護一個隊列來儲存乘客的請求,并且由于完全按照FIFO的政策,是以我們可以用java.concurrent類中的LinkedBlockingQueue,其中,排程器其實是一個靜态類,但可以優化成單例模式。

private static LinkedBlockingQueue<Object> queue
        = new LinkedBlockingQueue<>(31);
           

​ 再設計一個電梯,負責從排程器中取用請求,并将乘客帶往目的地。

​ 在沒有請求但是輸入沒有結束的時候,LinkedBlockingQueue的take方法自帶阻塞,在請求輸入結束以後,我會給隊列中加上一個String對象,當電梯接收到這個String對象的時候,就會自動退出線程,運作結束:

public static PersonRequest requestService() {
    try {
        Object m = queue.take();
        if (m instanceof String) {
            return null;
        } else {
            return (PersonRequest) m;
        }
    } catch (InterruptedException e) {
        return null;
    }
}
           

2.1.2 複雜度分析(class and method)

類圖:
面向對象程式設計第二單元總結(電梯系列)

​ Main類中隻有一個程式的入口點,負責接收輸入并把它添加到隊列中。

​ RequestQueue類中隻有兩個方法,一個負責添加請求,另一個負責給電梯請求。

​ Elevator類中有幾個特征方法,開門,關門,以及前往樓層,人的進出等。

複雜度分析:

​ 類複雜度

面向對象程式設計第二單元總結(電梯系列)

​ 方法複雜度

面向對象程式設計第二單元總結(電梯系列)

​ 可以看出,本次作業的結構複雜度、循環負責度和依賴度都比較低,也許是需求比較簡單的緣故。

​ 優缺點分析:

​ 個人覺得這次作業的設計還是比較好的,因為每個方法的各項複雜度都很低,并且每個類職能很專一,都隻做自己該做的事情,比較好地展現了oo的思想。

2.1.3 時序圖分析

面向對象程式設計第二單元總結(電梯系列)

​ Main負責給queue請求,而queue和elevator互動,電梯去拿請求,排程器則負責發送終止信号。

2.1.4 soild法則分析

​ SRP方面,這次作業還是實作的比較好的,每個類職責單一明确。

​ OCP方面,也實作的比較好,因為在下一次作業裡隻需修改Scheduler的内容和Elevator的内容而無需重寫。

2.2 第六次作業(ALS電梯)

2.2.1 設計政策

​ 第六次作業其實也相當于第五次作業的優化版,有點不同的是,在處理捎帶請求的時候可能需要周遊隊列,是以我不再采用LinkedBlockingQueue,而是自己通過Arraylist和同步語句塊來實作線程安全。并且采用了單例模式,確定隊列隻有一個并確定線程安全:

private static Scheduler instance = new Scheduler();
private ArrayList<Object> requestQueue = new ArrayList<>();

private Scheduler() {

}

public static Scheduler getInstance() {
    return instance;
}
           

​ 在排程器裡實作一個捎帶方法,即通過特定的條件來判斷隊列裡是否有符合條件的捎帶請求,如果有的話則加入到電梯的服務隊列中,并且根據到達樓層進行排序。

private void arriving(final PersonRequest m) {
    int way;
    if (nowFloor < m.getFromFloor()) {//設定方向
        way = 0;
    } else {
        way = 1;
    }
    while (nowFloor != m.getFromFloor()) {
        upOrDown(way);
    }
    if (m.getFromFloor() < m.getToFloor()) {
        way = 0;
    } else {
        way = 1;
    }//先到達起始樓層
    while (service.size() != 0) {//每到達一層即判斷有無捎帶請求
        service.addAll(Scheduler.getInstance().hasSameRequest(
                service.get(0), nowFloor));//添加捎帶請求
        checkService();//排序
        if (needOpen() && !doorStatus) {
            openDoor();
            service.addAll(Scheduler.getInstance().hasSameRequest(
                    service.get(0), nowFloor));
            checkService();
        }
        if (doorStatus) {
            letPersonIn();
            closeDoor();
        }
        upOrDown(way);//上下樓
        if (needOpen()) {
            openDoor();
            letPersonOut();
        }
    }
    closeDoor();
}
           

​ 當service隊列為空時,即主請求也服務完畢,則再從排程器隊列中取下一個請求。直到排程器隊列也為空。

2.2.2 複雜度分析

面向對象程式設計第二單元總結(電梯系列)

​ 其中,Main方法還是隻是負責接收輸入,把輸入給排程器。

​ 排程器是單例的,負責檢查是否有捎帶請求和給電梯請求。

​ 電梯依然是基本功能,隻是在此基礎上添加了服務清單,以及checkService(排序函數)

面向對象程式設計第二單元總結(電梯系列)
面向對象程式設計第二單元總結(電梯系列)

​ 可以看出,本次作業的設計各項複雜度也較低,因為隻是在第一個電梯的基礎上做了一些優化而已。同樣每個類的職能也很專一,隻負責自己改負責的部分。但其實為了更抽象一些,可以把Main的輸入請求作為一個線程放到排程器裡面,這樣更能展現oo的思想。

2.2.3 時序圖分析

面向對象程式設計第二單元總結(電梯系列)
和第一次作業類似。
           

2.2.4 solid法則分析

​ SRP方面,由于和第一次作業類似,是以實作較好。

​ OCP方面,由于第三次作業不太适合電梯去排程器拿請求,是以排程器就需要重構了,改為排程器給電梯發送請求,沒有實作擴充實作新功能。

2.3 第七次作業

2.3.1設計政策

​ 第三次作業和前兩次作業相比變得更複雜了,主要原因是有3部電梯,并且每部電梯都限制載客量,都有自己的可停靠樓層,這就需要乘客自行換乘才能實作,由于三個電梯隻是屬性不同,是以我依舊隻有三個類:排程器、電梯、Main類,這次,Main類隻負責啟動線程,而把輸入服務交給了排程器實作,排程器每接收到一個請求,就根據請求的樣例來分析發送給哪個電梯。如果發現這個請求無法由一個電梯完成,則将其拆成兩個請求,發送至可以完成第一個請求的電梯的服務隊列和轉發隊列中,當電梯到達轉發隊列的起始樓層并且乘客已經下電梯後,就把轉發請求給與對應的電梯。

​ 每個電梯的四個隊列,排程器不再維護總隊列,隻負責分發請求,隊列由電梯自己維護:

private ArrayList<PersonRequest> transfer = new ArrayList<>();
private ArrayList<PersonRequest> service = new ArrayList<>();
private ArrayList<PersonRequest> queue = new ArrayList<>();
private ArrayList<Integer> out = new ArrayList<>(); //who is already out.
           

​ 為了讓電梯間有互動,并且讓排程器知道其他電梯的資訊,排程器擁有三個電梯對象,這樣電梯間的互動就可以借助排程器來完成了。

private static Scheduler instance = new Scheduler();
private Elevator elevatorA;
private Elevator elevatorB;
private Elevator elevatorC;
           

​ 其他設計方法和第一第二次作業類似。

2.3.2 複雜度分析

類圖
面向對象程式設計第二單元總結(電梯系列)

​ Main:負責啟動線程。

​ TimableOutput:自行設計的線程安全輸出方法。

​ Scheduler:負責儲存電梯資訊和初始化電梯,提供靜态基本方法,主要方法為addRequest,其他方法基本上都是為了實作這個方法的方法。

​ Elevator: 基本功能和上次類似,隻是給了其他電梯添加轉發需求的權力,是以有了addTransferService等方法transferPerson為檢查是否有轉發需求的方法。

複雜度分析
面向對象程式設計第二單元總結(電梯系列)
面向對象程式設計第二單元總結(電梯系列)

​ 由于需求比較複雜,是以可以看到循環複雜度和依賴度都比較高,主要原因是需要多次周遊隊列以及需要其他線程的資訊。由于hashmap線程不安全,是以我還是采用了Arraylist,并自己添加同步方法

​ 優缺點分析:算法性能較差,循環複雜度高,但是每個類依然保持自己處理自己的事情,不過常用的靜态方法可以再新增一個類,而不用寫到排程器裡面,個人覺得這次作業還是符合oo思想的。

2.3.3 時序圖分析

面向對象程式設計第二單元總結(電梯系列)

​ main類負責啟動線程,排程器負責發送請求,電梯之間發送轉發請求進行互動。

2.3.4 solid法則分析

​ SRP方面,每個類依然有着自己明确的職責,實作的較好。

​ OCP方面,也實作的較好,如果電梯類型不同隻需修改構造器,排程器也無需重新構造。如果有新需求,隻需增加,無需重寫。

3.自己程式的bug

​ 本次作業中盡管我的設計已經分工明确,在前兩次的強測和互測中沒有被發現bug,但是在第三次作業中,因為一點粗心,即在發送轉發請求的過程中沒有考慮乘客是否已經下了電梯,錯了很多點,也被hack了很多下,但是還是在一次合并修複内把所有bug修完了。隻需要在電梯類中增加一個out隊列,記錄下已經下電梯的人,然後在轉發之前先找到隊列中是否有這個人再将其轉發即可,一個很微小的錯誤,而且也是邏輯上的錯誤,還是自己沒有測試充分、設計時沒有考慮清楚導緻。

4.發現别人bug采取的政策

​ 編寫自動化測試程式來對别人的程式進行測試,如果發現錯誤則進行bug定位,找到其出錯的位置,再考慮邏輯上還有沒有其他的bug,由于第七次作業我被分到c組,是以有效性較好,能根據錯誤快速定位bug所在位置,再根據其設計結構構造相似的樣例。

​ 線上程安全方面,主要測試是否有“電梯提前下班”和“接收不到輸入“的問題,隻需構造相應的測試樣例即可。

​ 在第一單元的測試中,我主要是通過找其輸入處理和輸出處理部分的bug,也就是字元串處理問題的bug,輸出量小,較為簡單,而本單元的測試輸出量大,就算有了輸出,自己肉眼看也很難看出輸出是否正确,因為輸出通常有幾百行,是以不得不編寫測試程式來測試,輔助找bug。

5.心得體會

5.1 線程安全

​ 由于多線程中采用的是并發執行的方式,是以可能會導緻線程不安全,本單元電梯系列有點像生産者-消費者模式。是以如何建構線程安全的程式呢?可以考慮以下幾點:

  • 對于不可變屬性,用final關鍵字修飾。
  • 對于嚴格隻允許一個程序通路的對象,使用單例模式。
  • 保證操作的原子性,使用Atomic類的變量。
  • 對于要處理臨界資源的方法或代碼塊,用同步語句修飾。
  • 要弄清楚wait() notify()方法應該放置于什麼位置。

5.2 設計原則

​ 從oo的角度來說,設計還是要符合oo的思想,每個類都嚴格地隻管自己該管的事情,方法也是如此。同時,要靈活運用java的内置類,裡面的好多資料結構一定是優于我們自己寫的程式的。比如這次就可以使用LinkedBlockingQueue,還有Lock與ReentrantLock的使用,比同步語句塊更靈活。ReentrantLock+Condition的組合可以實作喚醒特定的線程。其次,在設計的時候就要考慮盡可能多的方面,不然就會和我一樣強測拿到一個很低的分數。最重要的還是要秉承一類一組功能的原則,這樣在設計架構的時候可以保證很好的正确性。

5.3反思與總結

​ 本單元的作業自我評價不是很滿意,尤其是第三次作業的緻命bug,使我的強測拿了很低的分數,以後在寫完程式後不僅要進行邏輯性的檢查,還需要多加測試。盡管oo進度已經過半,但是還要不斷努力,繼續加油。