前言:
前三次作業用到了以下知識點:
1.類的創建與設計
2.類的方法實現
3.面向對象編程基礎思想
4.類的單一職責原則應用
5.正則表達式的使用
6.隊列管理 LinkedList 及其各個方法的使用
難度以及題量總結:
電梯調度問題:三次的電梯調度問題難度中等,基本邏輯並不複雜,但是要考慮的情況比較多。第一次電梯調度問題用了花費了5個小時,但是由於代碼並沒有符合面向對象編程的要求,第二次按照面向對象編程的要求花費了6個小時重新寫了一份,而最後一次修改前一次的代碼花費了20分鐘。通過這三次電梯調度問題,我更加理解了如何進行類的設計,知道了面向對象編程的好處,也更加了解了如何讓代碼更加符合單一職責性。在實踐當中我進行了正則表達式的使用,這也讓我認識到了java功能的強大。
其他題目:前三次題目除了電梯調度問題,整體難度偏低,基本上就是根據題目給的圖,將該類創出來,將類屬性裏的get方法和set方法寫出,再寫幾個計算結果的方法就能實現。題量不算大,且邏輯簡單,用的時間較短。完成這些題目讓我認識到java與c語言的區別,提升了我對類編程的熟悉度,也讓我更加理解了何為面對對象編程。
第一次作業
第一次電梯調度問題
作業要求:設計類以實現電梯的基本功能,需能讀取並處理內部請求(如電梯內乘客按下的目標樓層)和外部請求(如各樓層乘客按下的上行 / 下行按鈕),通過合理的類結構封裝電梯的樓層範圍、當前狀態、運行方向等屬性,以及請求隊列管理、調度邏輯等行為,從而模擬電梯根據請求完成移動、開關門等操作的過程。
類圖
第一次作業未考慮類設計,僅用單一電梯類
分析:在做第一次作業時對面對對象編程還不夠了解,代碼完完全全是服務於實現功能,僅用了一個類實現,且創了太多變量,變量名且方法名混亂難以看懂,不符合規範,現在回頭看寫的非常糟糕了。
代碼複雜度
從環形圖可看出,代碼註釋率較低,僅有4.2,最深代碼塊嵌套了 5 層,代碼可讀性較差,而且僅一個類完成了所有的功能,導致該類方法過多,不符合面向對象編程,且方法 Lift.judgeLiftDown() 的邏輯分支過多。優點是平均方法複雜度較低,代碼多數方法邏輯簡單,代碼較容易維護。
思路講解:
在做該問題時由於我對面對對象編程還不夠熟練,就只使用了一個電梯類和main類實現,在該類裏完成了電梯的所有功能。主要思路如下
隊列設計:我分別創建了IQ數組和OQ數組,再定義了OQ.front,OQ.rear,IQ.front,IQ.rear,來讓這幾個數組實現隊列的功能。
核心邏輯:這段程序核心方法是updateStatus,功能是完成一次上升一層或下降一層或停下來判斷下一步的方向。
第一步:每次進入updateStatus,都會先判斷當前方向是否有方向相同,樓層相同的外部指令或樓層相同的內部指令,若有則將全部符合的隊頭出隊。
第二步:判斷同方向上是否有方向相同的指令,若有則改變樓層,並將reststus設為1。
第三步:進入judgeStop判斷restaus是否等於1,若為1再計算判斷離的最近的指令從而決定下一步電梯的方向,若restaus等於1且當前有方向不同但是樓層相同的外部指令,則將nextstatus設為1,這樣在下次進入updateStatus就會不執行前兩步直接執行第三步,這是因為一次judgeStop只能判斷一條指令,使用nextStatus重複進入judgeStop就可以保證在靜止狀態下不會有同樓層的指令沒有出隊。
點擊查看代碼
public void updateStatus()
{
if(nextstatus==0)
{
this.status=1;
System.out.println("Current Floor: "+this.floor+" Direction: "+d_str[direction]);
this.doorOpen();
this.judgeIQ_add();
this.judgeOQ_add();
this.judgeliftUp();
this.judgeliftDown();
}
if(this.status==0&&(OQ_rear!=OQ_front||IQ_rear!=IQ_front))//停止狀態
{
this.judgeStop();
}
}
用liftMove循環執行updateStatus直到IQ和OQ為空時停止,這樣就能實現電梯循環達到所有樓層。
點擊查看代碼
public void liftMove()
{
while(this.IQ_rear!=this.IQ_front||this.OQ_rear!=this.OQ_front)
{
updateStatus();
}
}
踩坑心得
1..在用題目給的輸入案例測試代碼時並沒有出現問題,但是在pta提交時遇到了錯誤非零返回
我懷疑是由於輸入不合法導致的
點擊查看代碼
while ( (str.equals("end")) {
str = input.nextLine();
lift.orderGet(str);
}
若是該段代碼輸入一個空行可能導致程序異常
為了解決這個問題,我進行了.hasNextLine()的判斷
點擊查看代碼
while (input.hasNextLine()) {
str = input.nextLine();
if (str.equals("end")) {
break;
}
if (str.equals("")) { // 跳過空行
continue;
}
lift.orderGet(str);
}
之後遇到空行也可以正常輸出了
2.由於方法名以及變量名用了一些簡稱,當時寫看的懂,後面隨着越來越多,之前的變量名都混亂了,且註釋過少,閲讀非常困難。
3.在判斷電梯是否可以上升的指令中出現錯誤
點擊查看代碼
if((this.getIQ_floor()>this.floor)&&this.IQ_rear!=this.IQ_front||this.restatus==1)this.floor++;
代碼並非輸出錯誤,而是運行異常,連結果都沒輸出,然而已經進行過隊列是否為空的判斷了。
根據查詢發現由於我在if語句裏把判斷是否隊列已空寫到了比較隊列大小後面,這就導致了還沒判斷是否為空就進行了提取方向操作,由於進行了空指針操作從而出錯。
if((this.IQ_rear!=this.IQ_front&&this.getIQ_floor()>this.floor)||this.restatus==1)this.floor++;
以前我一直以為if裏的步驟都是同優先級同時執行的,該錯誤解開了我的誤區。
改進建議:
1.拆分單一職責類,避免一個類完成所有功能
2.保證變量和方法名稱符合規範,不要為了圖方便簡寫
3.代碼可讀性差,應該多些註釋
第二次作業
第二次電梯調度問題
作業要求:對之前電梯調度程序進行迭代性設計,目的為解決電梯類職責過多的問題,類設計要求遵循單一職責原則(SRP),要求必須包含但不限於設計電梯類、乘客請求類、隊列類以及控制類。
電梯運行規則與前階段單類設計相同,但要處理如下情況:
1.乘客請求樓層數有誤,具體為高於最高樓層數或低於最低樓層數,處理方法:程序自動忽略此類輸入,繼續執行
2.乘客請求不合理,具體為輸入時出現連續的相同請求,例如<3><3><3>或者<5,DOWN><5,DOWN>,處理方法:程序自動忽略相同的多餘輸入,繼續執行,例如<3><3><3>過濾為<3>
類圖
本次代碼拆分之前的lift類為多個類
Elevator類
ExternalRequest類
RequestQueue類
Controller類
分析:這次我對類進行了重新設計,將原本單一核心類改為了多個類,也對原本的一些變量的名稱以及方法名進行了調整,如將原本的nextStatus改為了現在的shouldmove,這讓我的代碼更加清晰直觀。我還對原本一些複雜的方法進行了拆分,如原本需要judgeStop來完成對所有靜止情況的判斷,本次重新設計我用了多個職責單一的方法來解決該問題。雖然這次重新設計代碼量增加了很多,但是後續代碼維護與更新也更輕鬆了。對比上次有很大的提升。
代碼複雜度
分析:可見對比上一次雖有所優化但仍有不足
優化:
模塊化設計大幅進步:類和接口數量從 2 增加到 7,每類方法數從 8.50 降至 5.43,這説明你對代碼進行了合理的拆分,讓每個類的職責更單一,符合面向對象設計的 “單一職責原則”,後續維護和擴展會輕鬆很多。
可讀性加強:雖然註釋率現在有6.9%仍然不夠多,但對比上一次4%以經有了一定的提升,代碼可讀性得到了增強了
代碼邏輯清晰:平均複雜度從 3.40 降至 1.93,最大複雜度也大幅降低,代碼邏輯更加清晰了。
不足:
塊深度增大了,可能還是有些不必要的嵌套需要後續排查。
思路講解:
本次代碼基本思路直接迭代作業一,只是對部分邏輯進行了修改
1.電梯狀態邏輯的改變:第一次作業中電梯的狀態是MOVING還是STOP僅僅只是擺設,進入靜止狀態檢測靠的是變量restatus,本次修改我將所有restatus刪除,改為了判斷當前的state是否為STOP,這讓我的代碼可讀性更高,也減少了不必要的冗餘。
2.隊列的轉變:原本我的隊列是用數組實現的,而現在我直接用java的LinkedList實現隊列,並創建一個ExternalRequest類處理外部請求的隊列
3.過濾連續重複命令:思路非常簡單,原本我是用if判斷,每次移動最多出隊一次,只要使用while指令,就能一次把所有符合標準的隊頭出隊,自然也可以過濾連續相同指令了。
點擊查看代碼
while(!this.queue.getInternalRequests().isEmpty()&&this.elevator.getCurrentFloor()==this.queue.getInternalRequests().getFirst())//使用while指令一次把所有符合標準的出隊
{
this.queue.getInternalRequests().removeFirst();
this.next_is_specialOQ=false;
}
4.忽略樓層號有誤的指令:在每次把新指令加到指令隊列之前先進行合法性判斷,若高於最高樓層或低於最低樓層則跳過該指令不加入隊列中。
點擊查看代碼
public boolean isVaildFloor(int floor)
{
if(floor>=this.minFloor&&floor<=this.maxFloor)return true;
else return false;
}
踩坑心得
將新的外部請求加入到外部請求隊列時報錯
點擊查看代碼
queue.addExternalRequest(Integer.parseInt(matcher.group(1)),Direction.DOWN);
這是因為外部請求的隊列所用的參數是類externalRequests,隊列無法將我傳的兩個參數自動識別為構造了externalRequests,因而無法實現。
只需要在加入隊列時先構造出externalRequests,再加參數入隊即可。
點擊查看代碼
public void addExternalRequest(int floor, Direction direction)
{
this.externalRequests.add(new ExternalRequest(floor,direction));
}
改進建議:
1.註釋依舊過少,代碼可讀性還需提升。
2.塊深度過高,應進行更多邏輯的檢查。
第三次作業
第三次作業調度問題
作業要求:
電梯運行規則與前階段相同,但有如下變動情況:
乘客請求輸入變動情況:外部請求由之前的<請求樓層數,請求方向>修改為<請求源樓層,請求目的樓層>
對於外部請求,當電梯處理該請求之後(該請求出隊),要將<請求源樓層,請求目的樓層>中的請求目的樓層加入到請求內部隊列(加到隊尾)
類圖
分析:類設計基本與上次一致,將原本的ExternalRequest類改為了Passenger類,這樣命名更符合要求。同時在Passenger類里加入了destinationfloor變量以解決新的問題。
代碼複雜性
分析:
這次代碼邏輯上對比上次並沒有修改太多,主要進行的是註釋增加以及功能的增加。修改有以下:
1.註釋量明顯提升:從原來的註釋率6.9%轉變為9.4%,可讀性加強
2.塊深度優化:最深塊深度從 6 降至 5,平均塊深度從 1.76 降至 1.67,嵌套邏輯更簡單。
3.方法複雜度優化:最大複雜度從 10 降至 9,平均複雜度從 1.93 升至 1.80。
思路講解:
電梯基本移動規則直接迭代上一次的代碼,本次修改不過是加了一下新的功能
核心思路為以下:
1.將原本的外部指令格式從<floor,direction>改為了<floor,destination floor>
2.在構造函數得到外部指令的樓層和目的樓層後直接計算出新的外部指令的方向
點擊查看代碼
if(floor>destinationfloor)this.directinotallow=Direction.DOWN;//計算出方向
else this.direction=Direction.UP;
3.在外部指令出隊前先將外部指令隊首1的destinationfloor存下來,再把destination存到內部指令隊尾,再進行出隊操作
踩坑心得
代碼中外部指令出隊操作太多,在每個外部指令出隊操作前面一個一個加到內部指令太麻煩。
直接在RequestQueue類里加入 removeExFrist方法,以後外部隊列出隊直接使用該方法
點擊查看代碼
public void removeExFrist()//進行外部指令的出隊操作
{
this.addInternalRequest(this.externalRequests.getFirst().getDestinationFloor());//先要把這條外部指令加到內部指令隊尾
this.externalRequests.removeFirst();//移除這條外部指令
}
改進建議
本次並沒有過多之前的代碼邏輯,主要是增加了新功能,後續還可以修改以下方面:
1.代碼複用進一步提升:將Controller類中的重複邏輯,封裝為工具方法,減少冗餘。
2.擴展性設計:可通過抽象類定義電梯通用接口,後續支持普通電梯、貨運電梯等不同類型時,無需修改核心調度邏輯,這樣擴展性也會更強。
心得體會
在完成這三次作業前,我哪怕是寫java的題目還是隻會使用“C語言的面向過程思維解決問題”,而經過這三次作業後,我真正瞭解了何為“面向對象編程”。第一次作業因缺乏設計思維導致代碼混亂難以看懂,這次教訓讓我意識到單一職責原則以及類設計的重要性;第二次作業的重構過程中,我將原本的一個解決所有問題的類拆成了多個類,雖然代碼量增加了很多,但是可讀性卻更好,更容易看懂了,我真切體會到 “封裝” 如何讓代碼結構更清晰、職責更明確。同時我使用java本身含有的隊列創建方法,也讓我體驗到java的便捷。而第三次作業,由於之前類設計的完善,代碼僅需微調類結構、擴展方法即可實現新需求的經歷,這讓我感受到模塊化設計帶來的可擴展性。這次作業給我最大的教訓就是,一定不要圖方便,進行面向過程的編程就結束,如果是大作業的話,面向對象設計更優,後續維護也會更便利。最後對於題目測試點太少了,感覺哪怕代碼有錯誤也有可能運氣好過了,要是代碼有一點問題可能就一分都沒,建議老師在pta多加一點測試點。