「BUAA OO Unit 4 HW16」第四單元總結與課程回顧
- 2022 年 6 月 29 日
- 筆記
- BUAA-OO, Java Programming
「BUAA OO Unit 4 HW16」第四單元總結與課程回顧
Part 0 第四單元作業架構設計
架構設計概要
本單元的設計目標為擴展UML解析器,使之支援對UML類圖、狀態圖和順序圖的分析,可以通過輸入相應的指令來進行相關查詢,並能根據UML規則進行一定的規範性驗證。
整個第四單元的三次作業是依次迭代的,沒有進行重構,因此這裡以第15次作業為例介紹架構設計如下圖示。

在本次作業中,我主要遵循以下幾個原則進行設計:
- 將所有
UML*
封裝為My*
,以便於統一管理和增設屬性。 - 將數據管理功能抽離出來,形成
MyDataBase
,其他類需要使用數據時,只需將引用指向MyDataBase
中對象即可。 - 將規範性檢查功能抽離出來,形成
MyCheckForUml
。 - 根據輸入的特點,遵循靜態圖構建->查詢並記憶化維護間接屬性的原則進行程式碼實現。
上述原則是在作業實現中逐步摸索出來的,以下以AppRunner官方包程式碼簡析與架構設計初步、層次化循環讀入建模————以泛化和介面實現為例和基於靜態圖的架構分析、設計與實現逐步深入介紹我對第四單元認識從稚嫩到成熟的過程。在這一部分的最後,我還將介紹異常處理的層次化這一小技巧。
AppRunner官方包程式碼簡析與架構設計初步
本部分由兩部分組成:
- 第一部分通過分析AppRunner具體程式碼清晰地了解官方包如何實現相應的介面和參數傳遞。這部分有利於理清官方包的實現,了解黑盒程式如何通過輸入得到輸出。
- 第二部分基於第一部分對官方包的理解,界限分明地標識我們工作的範圍和需要完成的工作,並提出了一種可能的設計思路和架構,以供參考。
一、AppRunner工作邏輯和流程
將閱讀官方包的收穫記錄在此
1. 屬性
1.1 interaction
我們完成的UML解析器的實例化對象
1.2 status
標記此時AppRunner行為
2. 行為
主體運算邏輯在run
中。
2.1 run
public void run(String[] args) {
try {
beforeStartEvent();
Scanner scanner = new Scanner(inputStream);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
lineProcessEvent(line);
}
scanner.close();
afterCompleteEvent();
} catch (Exception e) {
exceptionProcessEvent(e);
}
}
以下步驟均在AppRunner中實現
為方便表示,下文用MyImplementation
指代我們實現的UML解析器。
Step 1 輸入與解析模型
run
方法中調用beforeStartEvent
方法,設置當前狀態為PROCESSING_MODEL
- 在
lineProcessEvent
中,對傳入的每行字元串進行解析,將蘊含的模型資訊通過modelProcessEvent
處理為UmlElement
對象,並存入elementList
容器 - 當讀到字元串
END_OF_MODEL
時,判斷模型解析過程結束 - 調用
endOfModelProcessEvent
,設置當前狀態為PROCESSING_INSTRUCTION
,並將elementList
中的數據作為參數傳給MyImplementation
Step 2 輸入與指令獲取
經過 Step 1 ,當前狀態為
PROCESSING_INSTRUCTION
- 仍在
lineProcessEvent
中運行。由於當前狀態為PROCESSING_INSTRUCTION
,進入else if
分支 - 對於每行非空指令,調用
instructionProcessEvent
解析 - 在
instructionProcessEvent
中,調用runAsArguments
解析,通過預先設置好的PROCESSORS
映射,調用該指令類型的處理函數,如runAsClassCount
Step 3 輸出
經過 Step 2 ,我們已經將每條非空合法指令傳入其對應的解析方法中
- 在具體指令處理函數(如
runAsClassCount
)中,調用MyImplementation
的相應方法(如getClassCount
),獲取返回值並按規格列印
二、我們的工作
1. 解析和存儲元素
經過上述分析,我們知道AppRunner在run
中通過Step 1將儲存UmlElement
對象的容器作為參數傳給了MyImplementation
。因此,我們需要解析這些UmlElement
對象。
1.1 輸入順序
值得注意的是,輸入並沒有保證順序。也就是說,兩個類的繼承關係的指令可能早於這兩個類的指令,即元素的依賴不一定早於元素出現,即亂序。
因此,一個不錯的思路是多輪遍歷讀入:設計三個獨立的循環,第一輪處理UML_CLASS
、UML_INTERFACE
和UML_ASSOCIATION
;第二輪處理UML_ATTRIBUTE
、UML_OPERATION
、UML_ASSOCIATION_END
、UML_GENERALIZATION
;第三輪處理UML_PARAMETER
、UML_INTERFACE_REALIZATION
、UML_GENERALIZATION
。
通過上述的三輪循環遍歷讀入,我們保證了自上向下建模。
我這裡還沒有想得很清楚,歡迎大家批評
1.2 元素封裝
官方包提供的元素類並不能直接完全回答作業要求的查詢問題,一種可行的辦法是自行為每一種元素封裝單獨的類,並通過容器將其組織起來,形成一棵類似第一單元表達式樹的元素樹。
舉例而言,對於UmlClass
,我們封裝MyClass
繼承(或者將UmlClass
對象作為MyClass
類的一個屬性)其,增加Myclass parent
,ArrayList<MyClass> sons
等屬性,儘可能豐富地表達其特徵,方便後續查詢。
2. 查詢
AppRunner的run
在將模型資訊傳入MyImplement
後,會解析每一條查詢指令並調用MyImplement
的相應方法。
通過上一步中的解析和存儲元素後,我們可以在其中直接維護一些查詢量,對於不便維護的查詢量,也可以通過我們完善的元素樹方便地獲取。
層次化循環讀入建模————以泛化和介面實現為例
背景
傳入MyImplement
的參數UmlElement... elements
不保證順序,因此需要多輪讀入。這裡,介紹一種層次化循環讀入建模的設計架構。
這裡主要針對類和介面的泛化與介面的實現,對於作業中其他的屬性以及後續作業的新要求,可以利用類似的思想
示意圖

上圖展示了三輪循環的工作實現與要點,以下結合該圖逐個介紹。
流程
Round 1 「實體」加入
第一輪遍歷,加入類和介面實體,這時候泛化和介面實現等關係尚未加入,彼此處於孤立狀態。
Round 2 「泛化」加入
第二輪遍歷,將泛化關係加入圖中。
值得注意的是,我們在本層循環動態維護了每個類的所有子類和所有父類,每個介面的所有子介面和所有父介面。在本輪循環結束時,我們可以通過 $ O(1) $ 的複雜度查詢每個類(介面)的子類(介面)和父類(介面)資訊(其實這是一種ensures
)。
在這一輪中,值得注意的是實際上我們形成了類和介面兩片森林,他們之間還沒有發生關係。
Round 3 「介面實現」加入
第三輪遍歷,將「介面實現」加入圖中。
在本輪遍歷中,我們實際的操作類似於進行樹的合併,在合併的時候應當注意動態維護類所實現的所有介面,類似於「壓縮」思想:即每個類儲存了所有實現介面的資訊,通過 $ O(1) $ 查詢即可,無需通過查找樹到根以獲得父親資訊。
基於靜態圖的架構分析、設計與實現
在上文中,我們介紹了在對圖建模過程中動態維護間接屬性從而以 $ O(1) $ 代價完成查詢操作的層次化循環讀入建模思路。
在本文中,依託前兩篇文章的工作,我們介紹一種基於靜態圖的架構分析、設計與實現。
- Part 0簡要介紹本次作業的實際工作內容
- Part 1元素封裝以統一管理的方法
- Part 2架構分析
- Part 3架構設計
- Part 4架構優點
以上為文章概要,可自取所需。
本文基於前述兩篇文章繼續分析,有興趣的同學可以回顧。傳送門:AppRunner官方包程式碼簡析與作業架構設計初步、層次化循環讀入建模————以泛化和介面實現為例 。
Part 0 背景
本次作業的工作內容為:
- 給定不定長數組
UmlElement... elements
適當建模 - 給定指令查詢圖的相關資訊
明確上述兩點後,我們逐層展開分析。
Part 1 元素封裝
官方包給出了UML所有元素的數據結構,但是對於本單元作業而言資訊並不充分,一種值得推薦的方法是對所有UML元素進行封裝,保證了統一性。經zsm助教提醒,這裡突出強調了統一封裝,我們對此展開講講:對於一些UmlElement
,可能官方包提供的資訊已經較為充足,事實上無需另外封裝;但是封裝一部分而不封裝另外一部分,可能會出現需要不時確認此處應當用Myxxx
還是用Umlxxx
的困惑,不便於開發和維護。
在自行封裝的元素類內部,我們可以聲明各種有助於查詢的間接量,下文也將結合具體要求介紹如何實際應用。
Part 2 架構分析
靜態圖
根據上文對AppRunner的分析,我們知道首先傳入不定長的UmlElement
數組,接著輸入若干指令進行查詢。基於這樣的交互保證,我們可以把工作進一步抽象為:
- 根據不定長的
UmlElement
數組獲得一張靜態圖 - 根據給定指令查詢靜態圖相關資訊
值得注意的是,事實上,在第一條查詢指令之前,我們就已經對整張圖建模結束。此時,所有直接與間接資訊已經完全確定,只是可能還沒有計算出來。
記憶化
在靜態圖分析中,我們知道查詢時資訊已經完全確定,因此,針對這張圖的每次查詢都會獲得準確的答案,這使得我們可以利用記憶化的方式減小時間複雜度與棧的深度。
舉例而言,在一些指令中,如指令 7:類實現的全部介面,我們如果先查詢了某個類classA
實現的全部介面,在查詢其(直接)子類classB
實現的全部介面時只需要將classB
自己實現的介面與classA
查詢得到的介面做並集即可。
Part 3 架構設計與實現
接下來分為三步介紹架構具體設計與實現,其中的Step2和Step3事實上可以合併,這將在本Part最後介紹。
Step 1 靜態圖構建
根據Part 2 架構分析,我們知道,在靜態圖完全建模完成之前,間接屬性會隨著圖的完善而不斷變化,因此,在層次化循環讀入建模————以泛化和介面實現為例中提出的動態維護的思維複雜度主要原因在此。針對這一問題,我們首先完成靜態圖的構建,只維護直接屬性(如類的直接繼承、介面的直接實現等),對於可能在建圖過程中動態變化的資訊(如類實現的全部介面,類的全部直接與間接子類等)在本階段不做維護,以極大化簡思維複雜度。
確定了當前階段只維護直接屬性,我們依舊需要多輪讀入。以下提供一種分輪循環讀入建模思路並簡述其合理性:
Round 1 讀入實體
- UML_CLASS
- UML_INTERFACE
- UML_ASSOCIATION
這部分相當於在圖中建立節點,還沒有加入節點之間的關係
Round 2 讀入較獨立的關係
- UML_ATTRIBUTE
- UML_OPERATION
- UML_ASSOCIATION_END
- UML_GENERALIZATION
這部分讀入了和自己或和自己同類相關的關係
Round 3 讀入較依賴的關係
- UML_PARAMETER
- UML_INTERFACE_REALIZATION
這部分讀入了和如介面實現這種跨類相關的關係
Tip : 上述分輪只是一種大致思路,實際上並非嚴格順序依賴。舉例而言:UML_INTERFACE_REALIZATION只需要前置UML_CLASS、UML_INTERFACE讀入完成即可
Step 2 計算所有會被查詢的間接屬性
Part 3 Step 1 靜態圖構建中完成了圖的建構及所有必要直接屬性,這已經蘊含所有資訊,我們這一階段只需要根據需求遍歷檢索所有間接屬性即可。以下,我們以指令 7:類實現的全部介面為例介紹具體操作。
根據Part 2 架構分析中的記憶化思路,我們首先為MyClass
增加屬性Set<MyInterface> interfaces
,其含義為MyClass
對象所實現的所有介面,並在構造器中將其初始化為null
。接著,我們遍歷MyImplementation
中維護的MyClass
容器中的全部對象調用Set<Interface> getImplementInterfaceList
方法進行查找,該方法流程如下:
- 如果該對象的
Set<MyInterface> interfaces
非空,說明之前檢索過,直接返回interfaces
- 如果
Set<MyInterface> interfaces
為空,說明未曾檢索過,執行interfaces = myInterfaces.merge(this.parent.getImplementInterfaces)
。其中,myInterfaces
是在靜態圖構建中維護的類對象自己實現的介面。
經過這樣的遍歷,我們計算了所有MyClass
的實現全部介面,若再需要查詢,直接 $ O(1) $ 返回即可。
Step 3 查詢
在Step 2中,我們計算了所有可能被查詢的間接屬性,因此,對大部分指令,我們可以以 $ O(1) $ 返回。
上述Step 2和Step 3顯然可以優化:我們建圖結束後直接進入查詢部分,對於查詢過的資訊記憶化維護即可。
Part 4 架構優點
思維複雜度低
將實現分為了兩步走:靜態圖構建->查詢並記憶化維護間接屬性。避免了建圖過程中動態維護的繁瑣。
面向對象
我們沒有忘記OO課程的初心,儘可能地進行抽象與分層建模,為每個類設計狀態和行為,使之彼此協作,避免了類似一main到底的實現策略。
可拓展
得益於靜態圖的特點和實現架構,我們基本1:1模擬實現了UmlElement
提供的直接資訊,避免了建圖時動態維護屬性帶來的高複雜度和低可維護性。
異常處理的抽象與層次化
對於部分異常情況較多的方法,一次性直接處理顯然是困難而背離層次抽象原則的。因此,一種值得推薦的方法是處理本層次的異常,並調用下一層次的方法且將其他異常交由其處理。
舉例而言:getParticipantCreator
有六種異常,分別是:
-
InteractionNotFoundException
-
InteractionDuplicatedException
-
LifelineNotFoundException
-
LifelineDuplicatedException
-
LifelineNeverCreatedException
-
LifelineCreatedRepeatedlyException
其中,在MyImplementation
中,我們處理前兩個關於交互不存在或有重名交互的情況;得到正確的唯一交互後,在MyInteraction
中我們處理中間兩個關於生命線不存在或有重名生命線的情況;最後,在得到正確的唯一生命線後,我們處理不存在創建消息或重複創建的情況。
具體的方法原型可以參考如下:
MyInplementation
中
@Override
public UmlLifeline getParticipantCreator(String interactionName, String lifelineName)
throws InteractionNotFoundException, InteractionDuplicatedException,
LifelineNotFoundException, LifelineDuplicatedException,
LifelineNeverCreatedException, LifelineCreatedRepeatedlyException {
/* TODO */
}
MyInteraction
中
public UmlLifeline getParticipantCreator(String interactionName, String lifelineName)
throws LifelineNotFoundException, LifelineDuplicatedException,
LifelineNeverCreatedException, LifelineCreatedRepeatedlyException {
/* TODO */
}
MyLineLine
中
public UmlLifeline getParticipantCreator(String interactionName, String lifelineName)
throws LifelineNeverCreatedException, LifelineCreatedRepeatedlyException {
/* TODO */
}
Part 1 四個單元中架構設計思維和OO方法理解的演進
抽象
與Pre2中已經給出具體類如何設計不同,四個單元的作業內容不再給出如此具體的類搭建方法,需要一定的抽象能力。從第一單元的表達式,到第二單元的電梯,到第三單元的JML,到第四單元的UML,如何劃分清晰的狀態和功能特徵,給出合適的抽象類至關重要。一種好的抽象可以有效提高程式碼的魯棒性和健壯性,並降低程式碼重構的可能。
層次化
當使用面向對象的眼光來審視我們的任務時,模糊的層次化概念會在我們腦中樸素地浮現,但是,這是遠遠不夠的,我們需要更清晰,更具體的抽象與層次化構建,搭建出既精簡,又易擴展,還好維護的架構。第一單元的表達式層次化構建相對簡單,training中也給出了具體的表達式因子->項->表達式的層次,並暗含遞歸關係;第二單元的電梯中,我採用了相對簡單的無單獨調度器的設計,基本只有電梯,候乘區和輸入執行緒三種層次;第三單元中,JML規格已經限制了層次化設計;第四單元中,封裝My*
類、MyDataBase
類和MyCheckForUml
類的層次化設計令我受益匪淺。
封裝與解耦
OO課程引入的checkstyle是我最喜歡的課程工具之一。這個工具從量化角度限制了空白符的使用,換行的使用,import的使用,類、方法與文件的行數等形式化規約。雖然看似只是限制了表面的程式碼風格,但實際上暗含了解耦的需求:面向過程的一main到底的設計方式將不再可取!
四個單元的演進過程中,我對於解耦的理解也逐漸深化:對於什麼樣的共性功能,可以抽象出來一個公共介面?既具有公共行為,又具有公共狀態的類之間應如何設計關係?簡單的功能是否還需要封裝為一個方法,或者說複雜的方法應該解耦到多大粒度才合適?
除此之外,checkstyle確實讓程式碼風格變得統一而優雅,這對於互測時會檢查roommate程式碼源文件的同學是個不錯的消息。
設計模式
理論課介紹了相當多的設計模式,其中,工廠模式、單例模式和生產者-消費者模型我較為熟悉,應用較多。
工廠模式對於new
行為的封裝符合SOLID原則,給出了更一般化的生產對象方法。
單例模式將一個應用廣泛的對象的類封裝為唯一實例,避免了在不同類的方法之間相互傳遞該對象的引用帶來的繁瑣。在第二單元中,對安全輸出類的封裝讓我受益匪淺。
生產者-消費者模型是重要的多執行緒模型,在OS的理論課和實驗課中也多有涉及。在第二單元中,生產者-消費者模型的優點得到了淋漓盡致的表現,我在該單元的設計基本依託於生產者-消費者模型展開。
Part 2 四個單元中測試理解與實踐的演進
在OO作業中,對於每單元作業,我都以獨立或合作的形式完成了評測姬的設計與實現,這樣的鍛煉讓我收穫良多。
第一單元中,我主要利用python的科學計算庫sympy,直接隨機生成輸入數據,並計算答案的正確與否。同時,通過設置常量池,對於部分邊緣數據進行較為有效的覆蓋。
第二單元中,評測機主要採用隨機數據生成與狀態驗證的辦法,同時,由於本單元對CPU時間有較高要求,因此在linux中利用其計時工具檢查是否存在輪詢。
第三和第四單元,評測機主要採用隨機數據生成與對拍的方式進行驗證。這兩個單元中,官方數據的壓力較小,為了保證我們隨機數據的覆蓋性,我們加大了數據的強度,儘可能保證評測機有效性。
值得注意的是,上述評測機除了第一單元我親自全流程完成過一個外,其他大多為和同學合作/修改學長的評測機,不得不說這是一個非常耗時耗力的工作,同時,經驗尚淺的我很可能不能保證隨機數據的強度和覆蓋性。
儘管如此,搭建與修改評測機的過程依然收穫良多,隨機數據與邊緣數據的生成,調用合適的庫避免重複造輪子,與小夥伴對拍降低正確性驗證難度等都讓我在課程之外學到了更多知識,也更加讓我意識到測試的重要性:你不會指望未來的生產中也總有官方評測機測試你的程式碼!
Part 3 課程收穫
本學期的OO課程是大二以來體驗最好的一門課。作為一門核心專業課,它不愧對「重課」「難課」的頭銜,任務量與難度上都很難說輕鬆。但另一方面,這種「踮踮腳就可以夠得到」的核心專業課,既做到了讓我們走出舒適區,又不使課程難度過高而讓人望而卻步,為我帶來了巨大的收穫。
課程中老師、助教和同學們的悉心幫助是我能夠完成OO的重要原因,沒有如此負責而高水平的老師和助教,沒有熱心的同學們,就沒有完成了課程的我,在這裡再次向大家表示最誠摯的謝意。
OO課程中的閃光點令人驚嘆:
- 區別於某些高校,我們注重實戰訓練,每周平均1k行程式碼的程式碼量避免同學們止步於理論和空談。”Talk is cheap, show me the code”。
- 短平快的開發模式。一周一作業,三周一單元的開發進度恰屬於「大家踮踮腳就可以夠到」的水平。一方面,程式碼量保證了大家的訓練量不會小,另一方面,我們的難度又不會設置的太高,三次作業迭代下來的3k行程式碼初具工程特點,短平快的特性又使得訓練的性價比很高。
- checkstyle, git, starUML等現代工具的應用。我們擁抱了變化與未來,積極對接前沿的與主流的技術,checkstyle規範了程式碼風格,git規範了版本迭代與管理,starUML訓練了使用統一建模語言進行跨越程式語言與領域的溝通能力。
- 階梯式測試。中測、強測與互測的三個維度既兼顧了保證一般水平的同學可以通過本門課程,又保證水平更高的同學不至於「吃不飽」,在測試中提高面向對象的能力的理念也十分先進。
Part 4 課程改進建議
當下的OO課程已經十分優秀,但根據個人體驗及與同學的討論,或許以下方面仍有可以再進一步的空間。
預習課程
本學期OO Pre的體驗很不錯,特別是其中的pre1和pre2部分分別介紹了工具鏈與面向對象的基本思想。
關於pre1,希望可以增加簡單介紹如何打一個jar包並使用命令行或python進行運行的簡介,並可以在pre2中介紹利用這種方法進行自動化數據測試。
關於pre2,希望可以在現有基礎上增加一個設計數據生成器與搭建自動評測(對拍)機的環節,鼓勵同學們在pre階段就能完成架構設計->程式碼編寫->評測(對拍)機搭建->自動化測試的流程,為正式課程奠定基礎。同時,可以增加幾個簡單的異常拋出例子,比如在本學期的冒險者遊戲中,可以設置如果使用了不存在的武器則拋出異常。
關於pre3,根據不嚴謹的身邊統計學觀察,大家在unit1中大多選擇了遞歸下降法,因此也可以在pre3中用一個簡單的例子介紹這種方法,並給出類似本學期第一次training的框架,或許可以有效降低第一次作業的挑戰性。
第二單元
- 訓練部分已經介紹樂生產者消費者模式和觀察者模式,這讓我受益匪淺,這一部分也可以增加介紹單例模式,尤其可以簡化調度器在各個類的傳遞。
- 本單元有同學完成了電梯的可視化,或許可以在訓練或實驗中增加該項目,給出大致框架後使得同學們擁有一個自己的電梯可視化程式碼。
第三單元
- 可以增加介紹在vscode中使用Java+JML的語言模式來高亮JML,減輕閱讀注釋程式碼難度,具體效果可以參考//oo.buaa.edu.cn/assignment/356/discussion/1194
- 可以增加小測試,讓同學們對某幾個方法的JML用自然語言描述,並比較JML和自然語言的優劣,並可以適當引入UML進行更大範圍的比較
第四單元
- 在「測驗」中可以增加對單元架構的理解,如「AppRunner」的運行模式等,確保同學們對單元要求有基本的正確理解。
- 可以增加官方包程式碼導讀文檔,如介紹各個package大致功能以及協作關係,或以小測試形式進行檢驗。
Part 5 後記
本學期的OO課程到此告一段落,回首一學期的四個月,每周的生活里都留下了OO的印記,儘管過程中不可不說遇到了相當程度的挑戰與挫折,但是在老師、助教和同學們的幫助下,我最終也得以幸運地完成了全部課程。在這裡,再次向大家表示誠摯的謝意。
儘管OO2022即將結束,但我與OO的緣分似乎還沒有走到盡頭,在接下來的一年裡,我將作為OO2023助教團隊的一員,通過參與課程建設的方式繼續OO之旅,和大家一起讓OO變得更好!