1. 程式人生 > 其它 >BUAA OO 第四單元( UML 解析器 )與課程總結

BUAA OO 第四單元( UML 解析器 )與課程總結

一學期的 OO 終於結課啦!

一、第四單元作業架構設計

第四單元整體是要求我們實現一個 UML 解析器。三次作業分別要求我們實現對類圖的解析,對順序圖和狀態圖的解析,對一些條規則進行合法性檢查。這一單元與上一個單元類似,解析器程式的大部分( json 解析,輸入命令解析等)均已經由官方包實現,自己只需要實現要求的幾個查詢介面並實現相應的查詢方法即可。

由於本單元對效能沒有過多要求( UML 模型中不超過 400 個元素,查詢指令不超過 300 條,並且後兩次作業的 CPU 時間均給了 10 秒),不太需要像上一單元一樣過多關注效能優化。因此本人僅採用最簡單的架構與最為暴力的演算法來實現作業要求。

第 13、14 次作業

架構非常簡單,也並未繼承官方包中給的 UML 物件類進行建圖等操作,只是直接將構造方法中給出的 elements 儲存為 HashMap 。雖然沒有進行建圖,但仍然進行了對繼承關係的預處理,將類和介面的繼承關係提前建立 Map 儲存起來。

在查詢方法中大量地使用 stream 操作來根據要求進行查詢 ( 在不考慮效能的前提下, stream 真香 )。下面舉一個簡單的例子:

// 假設已經用 Map 儲存了所有元素的資訊(元素 id 和元素物件本身對應)
private Map<String, UmlElement> elements;

// 取得狀態 state 經過一步轉移能到達的所有狀態的名稱
// UmlState state;
List<String> subsequence = elements.values().stream()
    .filter(umlElement -> umlElement instanceof UmlTransition)
    .map(umlElement -> (UmlTransition) umlElement)
    .filter(umlTransition -> umlTransition.getSource().equals(state.getId()))
    .map(umlTransition -> umlTransition.getTarget())
    .map(id -> elements.get(id)).map(UmlElement::getName)
    .collect(Collectors.toList());

上面的例子簡要展示了僅通過一系列 stream 操作而沒有繼承 UmlClass 等類進行建圖的前提下實現了 UML 解析器的相應查詢(例子較為簡陋,未考慮錯誤處理)

MyUmlHelper 類:將一些查詢過程中可能被多個方法複用的程式碼拆分成輔助用的方法(例如,根據類名取出類在 UML 模型中對應的元素物件(並能夠丟擲類不存在/類重名異常),取出某個類的所有方法/屬性,取出類的繼承鏈,根據變數型別物件 NamableType 的具體種類提取出型別名等)。本來打算一個 MyUmlInteraction 一類到底的,但是由於程式碼行數超過了 500 行,違反了 checkstyle 的要求所以將輔助用的方法單獨拆分成類。

第 14 次作業的頂層結構與第 13 次作業完全相同,未新增新的類,只是添加了相應的查詢方法。

第 15 次作業

在這次作業中新增了 UML 模型的合法性檢查。

同理,由於 MyUmlInteractionMyUmlHelper 兩個類均超過 300 行且接近 400 行,為了保證滿足 checkstyle 的 500 行以內的要求,將八個查詢方法單獨拆分成 MyUmlChecker 類。

另外,由於 R002 (禁止迴圈繼承) 與 R003 (禁止重複繼承) 均涉及到了與圖相關的操作( R003 採用 BFS,R002 採用 Tarjan 強聯通分量演算法),故不得不先建立繼承關係的有向圖。這裡採用了一個功能相對專一的容器類,以 UML 元素的 id 建立繼承關係圖,並實現了相應的搜尋演算法。

(由於架構實在過於簡單所以寫了很多文字湊一下字數...)

二、四個單元中架構設計及 OO 方法理解的演進

第一單元:這一單元主要研究的物件是表示式,由於考慮到需要對錶達式進行解析,並且在去年的資料結構中也學習過表示式樹,因此較為自然地想到了採用樹形結構來儲存表示式,根據表示式中成分的層次來設計物件的層次,每個物件對應著一個表示式成分,物件的行為對應表示式的行為。同時在解析表示式方面根據指導書給的文法形式化定義採用層次特點十分明顯的遞迴下降分析法來解析表示式。而對於求導、合併等具有共性的方法則將其抽象出來(成為介面,表示式成分分別實現這些介面)。這一架構的可擴充套件性十分良好,在三次作業中均未重構,同時遞迴下降分析法在錯誤格式處理上具有較好的健壯性。

第二單元:多執行緒程式設計,難點在於不同物件之間的協作,對共享資料的訪問控制等。這一單元相比第一單元引入了更多的設計模式(也許是由於多執行緒),例如生產者-消費者模式,狀態模式,策略模式等。多執行緒相關的坑(不知道算不算"架構設計")也是這一單元需要重點關注的,例如對共享資料的執行緒安全訪問,避免死鎖,利用等待喚醒機制防止輪詢等。(在多執行緒方面, Java 中已經有很多封裝好的物件,如 BlockingQueue , Semaphore, ReentrantLock, 等,善用它們可以規避很多執行緒安全的坑,從而將更多精力用在電梯的排程策略上。 媽媽再也不用擔心電梯吃人生孩子了。

第三單元:從這一單元開始架構設計相比前兩個單元難度降低了,這一單元是根據 JML 規格實現社交網路模擬,整體的架構(需要實現哪些類、哪些方法)都是由官方介面及其 JML 規格規定好的。需要自己設計的部分主要是容器的選擇以及對時間複雜度的控制。( JML 規格只是規定了程式的行為需要與規格一致,但並未規定採用的實現方式。例如規格中出現了陣列,但實現起來並不一定要用陣列,而可能採用 Map 等容器;規格中出現了巢狀的 \forall, \exists, \sum 等 "迴圈" 並不意味著實現也要是巢狀迴圈)

第四單元:這一單元在架構設計上似乎沒什麼可說的,就是要理解 UML 圖及其元素的含義,以及將官方包中相應的物件與 UML 模型中的元素對應起來。(也可能只是因為自己的設計過於簡陋,沒有根據 UMLElement 之間的關係自己建圖)

三、四個單元中測試理解與實踐的演進

第一單元:以自動測試為主,由於當時剛剛開學,時間較為充裕,故花了大量的時間編寫自己的資料生成器和自動評測機。但是資料生成器強度較弱,並且評測機的正確性檢查部分未檢查格式,因此單靠自己的測試無法規避第三次作業中可能輸出 sin(x*x) 的錯誤情況,僅測出了部分三角函式優化的 bug 。

第二單元:未搭建資料生成器與自動評測機,僅建立了一套簡陋的定時輸入發射器用於處理電梯的輸入。本地測試資料全部為手工生成。由於自己的電梯採取的是自由競爭的策略,沒有進行刻意的優化,並且全程採用執行緒安全容器,因此幾乎未發生執行緒安全相關的錯誤。本地測試的重點主要針對導致 CPU TLE 的元凶 "暴力輪詢"(往往在不經意間就發生了),手工構造的測試資料也大多為若干波請求中間隔有空窗期的模式來測試是否發生輪詢。

第三、四單元:由於這兩個單元細節特別多並且有唯一的正確答案,因此測試方案以多人對拍為主,手動構造邊界樣例為輔。對於第三單元,手工構造的測試用例主要針對若干效能瓶頸的方法,通過極端資料測試是否發生 TLE 。而第四單元構造資料則著眼於各種正確的、錯誤的、邊界的可能情況,由於不需要考慮效能所以想到的每種情況均只需構造 1 組資料測試行為是否正確即可。

四、課程收穫

預習階段:

  • 鞏固了一些 Java 語法
    • 容器的使用
    • stream (感謝 roife 的有關 stream 的分享)
    • 異常處理
    • 正則表示式
  • 體會了面向物件思想之封裝

第一單元:

  • 根據層次設計類、抽象類、介面及其繼承/實現關係
  • 遞迴下降分析法
  • 工廠模式
  • 用計算機實現在數學中早已習以為常的表示式優化
  • "圈複雜度" 等衡量面向物件程式的指標

第二單元:

  • 多執行緒程式的設計
  • Java 中多執行緒相關庫的使用
    • 同步互斥 synchronized 與等待喚醒 wait/notify
    • 執行緒安全容器 BlockingQueueConcurrentHashMap
    • 可重入鎖 ReentrantLock
    • 訊號量 Semaphore
  • 提升多執行緒程式的效能
    • 減少 "輪詢" 對 CPU 資源的浪費

第三單元:

  • JML 語法,理解 JML 規格
  • 提升程式效能,減少演算法時間複雜度
    • 根據需求選擇對效能最有利的容器
    • 空間換時間,記憶化結果防止重複計算
    • 複習了圖最短路演算法

第四單元:

  • 理解了 UML 的含義
  • 增強了理解謎語人指導書的能力

工具:

  • Intellj IDEA 整合開發環境
    • 程式碼風格檢查外掛 checkstyle
  • 版本管理工具 git 及程式碼託管倉庫 gitlab 的使用
  • 效能分析工具 JProfiler
  • 單元測試庫 JUnit

五、對課程的建議

  1. 在每單元的第一次作業前發放一些補充的預習資料(例如在第一單元前(寒假)補充遞迴下降分析法,第二單元前補充多執行緒程式設計以及相關庫的使用等,第三、四單元分別提前一週發放 JML 與 UML 的相關資料),從而減少每單元第一次作業時由於對相關的背景知識不熟悉造成的跨度大和痛苦。
  2. 對於指導書中的 "謎語人" 之處(即需求描述不清,有歧義之處)希望可以描述得更明確一些,尤其是 UML 這細節特別多的單元。刻意增大指導書的理解難度讓學生去"猜需求" 除了浪費時間以外恐怕也沒什麼收穫。
  3. 適當降低前兩個單元的效能分佔比,讓感興趣且學有餘力的同學去盡情優化效能(保留效能相關的獎,或者效能優秀者額外加分),但是不要讓效能以及效能分帶來的 "卷" 給很多同學們帶來焦慮。
  4. 希望實驗課能夠有評測反饋,課後保留指導書並提供參考答案。(否則實驗課沒什麼收穫,沒有評測反饋也沒有參考答案始終感覺不明不白)如果實驗課承載著某種考核/考試目的的話,可以在實驗課結束後公佈評測結果/參考答案。
  5. 每單元公開一些同學的優秀程式碼供大家參考學習(據說去年有這個環節但是今年沒了)。曾經在討論區中有助教表示課程組不鼓勵直接分享/交流程式碼,但是對於一些值得學習的 best practice 放出來供大家學習(而不是抄襲)未嘗不是壞事(對於基礎差一些的同學來說,把思路轉化為程式碼的過程還是很困難的,很有可能自學的方向完全歪了而不自知,也能過評測但是寫法完完全全不優雅)。
  6. 希望討論區可以支援關鍵字檢索或者標籤功能,目前的討論區如果內容多起來則找到某個特定的前人問過的問題(例如關於某個方法或者某個異常的問題)是很低效的。另外希望能有一個公共的討論區(整個課程總體、每個單元總體)用來進行不針對某次特定作業的通用討論,如關於 Java 語法 / git 使用 / IDEA 使用等主題。