面向物件第一單元總結回顧
OO第一單元總結回顧
目錄1.前言
OO第一單元的作業圍繞表示式化簡。考慮到增量式開發需要程式具有良好的可拓展性,三次作業都採用遞迴下降的方法,表示式、項、因子逐級分析,再針對不同層次採用不同的化簡、合併策略。下面對各次作業展開分析。
2.各次作業分析
2.1第一次作業
Part 1. 基本思路
第一次作業要求讀入一行表示式,展開其中的所有括號後輸出。資料限制包括:括號深度最多一層,指數不為負數等。效能分數則根據輸出字串的長度決定,越短越高。
第一次作業建立的遞迴下降架構是後續作業的基礎,參考了實驗課提供的遞迴下降例程,本人的程式類圖如下:
綠色框中的類用於解析表示式,橙色框中的類用於構成表示式樹。
程式對錶達式的處理步驟如下:
- 首先以字串形式的表示式建立
Lexer
物件,並由Lexer
物件建立Parser物件; - 呼叫
Parser
物件的parseExpr()
方法,後者遞迴呼叫parseTerm()
方法,parseFactor()
方法,建立表示式樹,並返回建立的Expr物件; - 呼叫
Expr
物件的delPower()
方法,將所有指數大於1的表示式化簡為多個表示式相乘; - 呼叫
Expr
物件的delBracket()
方法,使用乘法分配律展開括號; - 呼叫
Expr
物件的merge()
方法,將表示式的各項同類項合併,並輸出化簡後的表示式。
Part 2. 優化點
由於作業一實現相對容易,我轉而在優化上投入了大量時間,儘可能地達到最佳效能。採取的優化策略包括:
- 指數為0的項化簡為數字1;
- 合併所有同類項;
- 輸出時,引數為正數的項優先作為首項輸出,並省略其正號;
- 輸出時,指數為1的項不輸出指數;
- 輸出時,引數為1且含有變數x的項不輸出引數;
- 輸出時,指數為2的冪函式,不輸出
x**2
,轉而輸出x*x
Part 3. 類方法分析
使用MetricsReloaded工具分析各類的方法,得到如下結果:
以上圖片截取了複雜度最高的方法。分析表明,Expr
類的merge()
方法複雜度最高,這是因為該方法同時完成合並同類項和輸出結果的任務,任務過重,屬於設計時的缺陷;Parser
類的parseFactor()
方法複雜度較高,這是因為該方法需要同時考慮Number
類,Variable
類和Expr
類的解析;Term
類的delPower()
方法複雜度較高,這是因為該方法特判了指數為0的情況,比較繁瑣。
Part 4. 類分析
使用MetricsReloaded工具分析各類,得到如下結果:
分析表明,Parser
類的平均圈複雜度最高,這是因為Parser
類在解析表示式、項、因子時需要考慮許多細節問題,例如表示式是否包含指數等;Expr
類和Term
類的平均圈複雜度,總圈複雜度都很高,這是因為這兩個類的方法大多比較複雜,同時,兩個類方法過多,任務過重,也導致了總迴圈複雜度較高。
2.2 第二次作業
Part 1. 基本思路
第二次作業要求在第一次作業的基礎上增加了三種新的因子:三角函式、自定義函式和求和函式,程式功能拓展為展開所有的自定義函式和求和函式,並展開除三角函式呼叫的括號以外的所有括號。同時,第二次作業不再限制括號深度。
第二次作業的實現中,我增加了TriFunc
類用於表示三角函式,增加了CustomFuncManage
類和CustomFunc
類分別用於管理和處理自定義函式,增加了SumFunc
類用於處理求和函式。此外,為了滿足三角函式帶來的複雜化簡需求,增加了MergeTerm
類和MergeFactor
類用於同類項合併和化簡。
程式類圖如下(紅色方框中的類為本次新增的類):
程式的類主要分為三個部分:綠色框中的類用於解析表示式,橙色框中的類用於構成表示式樹,藍色框中的類用於表示式化簡。
程式的執行步驟與第一次作業類似,此處僅指出不同點:
- 在呼叫
Parser
類的方法解析時,parseFactor()
方法增加了對三角函式、自定義函式和求和函式的解析,其中後兩者會直接處理字串,返回替換後的表示式 - 為了減輕
Expr
類和Term
類的負擔,並實現三角函式合併,化簡工作交由MergeFactor
類和MergeTerm
類完成,兩個類都重寫了equals()
和hashCode()
方法,用於判斷兩項是否可以合併。
Part 2. 優化點
本次作業的優化策略相較於第一次作業,主要體現在增加了三角函式的合併。
Part 3. 類方法分析
以上分析僅截取了複雜度最高的部分方法。分析表明,Parser
類的parseFactor()
方法和Lexer
類的next()
方法複雜度較高,這是因為隨著因子種類的變多,兩個方法不可避免地需要增加分支來判斷新的因子;Expr
類的print()
方法和printFunc()
方法為了減少字串長度,設定了各種條件語句來匹配可以優化的地方;Term
類的delPower()
方法和MergeTerm
類的MergeTerm()
方法因為根據不同因子型別採用不同的化簡指數和合並策略,所以複雜度較高。
Part 4. 類分析
分析表明,Parser
類, Lexer
類, Expr
類, Term
類, MergeTerm
類的平均圈複雜度較高,大都因為這些類需要對不同因子分別判斷和處理。
2.3 第三次作業
Part 1. 基本思路
第三次作業要求在第二次作業的基礎上增加巢狀因子,允許任何因子充當三角函式和自定義函式呼叫的因子。簡單來說,程式需要能在解析表示式時遞迴解析另一個表示式。
為了支援三角函式巢狀因子,我增加了TriExprFunc
類。該類繼承TriExpr
類並支援三角函式內含表示式因子。我的程式類圖如下(紅色方框中的類為本次新增的類):
程式的類主要分為三個部分:綠色框中的類用於解析表示式,橙色框中的類用於構成表示式樹,藍色框中的類用於表示式化簡。
Part 2. 優化點
由於本次作業優化較為困難且容易出錯,本次作業未進行優化。
Part 3. 類方法分析
以上分析僅截取了複雜度最高的類方法。第三次作業類方法分析結果與第二次作業非常接近,在此不再論述。
Part 4. 類分析
類分析結果與第二次作業類似,新增的TriExprFunc
類複雜度較低,在此不再展開論述。
3 優缺點分析
三次作業的實現方法優缺點並存,分析如下:
優點:
- 採用遞迴下降方法,結構清晰,易於拓展;
- 設計的類邏輯合理,符合人類思維;各個類分工合理,基本做到高內聚低耦合;
- 採用了繼承和介面實現,增加了程式碼複用性和可讀性;
- 採用多種方法提升效能。
缺點:
- 部分類和類方法任務過重,缺乏分工,複雜度較高;
- 正確性和效能的實現相互分離,分別考慮,缺乏整體性。效能優化的程式碼非常冗餘;
- 未找到既能保持低複雜度,又能支援多種因子解析的方法。
4 Bug分析
4.1 自己作業的Bug分析
第一次作業中,我因為BNF描述閱讀不仔細,誤以為常數因子處於int範圍,導致Parser
類的parserFactor()
方法出現Bug,並花了較多時間發現這一問題。
第二次作業中,我本人並未發現Bug,卻在強測中被測出一個Bug:由於追求效能分,我在Expr
類的merge()
方法中將部分三角函式化簡為sin(x*x)
或cos(x*x)
的形式,然而這種形式並不符合BNF描述,所以未通過部分測試點。
第三次作業中,同樣因為BNF描述閱讀不仔細,我漏掉了自定義函式呼叫中,自定義函式名和'('中間的空白符,導致Parser
類的parserFactor()
方法執行時錯誤。
總結本次作業中我的程式碼出現的Bug,可以得到如下經驗:
- 形式化表述一定要逐字逐句仔細閱讀。
- 追求效能分的同時一定要確保正確性不受影響。
同時,經過對比,我發現出現Bug的地方均是圈複雜度較高的地方(Expr
類的merge()
方法,Parser
類的parserFactor()
方法),由此可見圈複雜度較高的程式碼可能會對程式設計師造成干擾,這體現了追求低複雜度的意義。
4.2 發現的同屋Bug
我在本單元中自行設計了評測機,用於自測和互測。評測機通過python實現,分為三個步驟:測試樣例隨機生成、執行jar包獲得程式輸出、比對程式輸出和正確輸出。在實現中,我使用了python的sympy包用於表示式化簡和比較,使用了subprocess包用於執行jar包。
通過評測機自動測試,我找到了同屋其他同學的一些Bug,列舉如下:
- 第一次作業:解析部分錯誤,導致形如
1*-1
這種乘法接負數的表示式出錯; - 第二次作業:為了追求效能,導致當正確輸出為0時不輸出;
- 第二次作業:解析部分錯誤,導致形如
sin(-1)
這種因子為負數的三角函數出錯; - 第三次作業:支援的資料範圍不滿足要求,
sum(i,s,e,t)
中的s和e不支援超長整數。
通過本次互測,可以總結出以下經驗:
- 通過評測機測試遠比人工測試更快速,有效;
- 同學們的錯誤主要集中於解析錯誤等,其根因在於形式化表述的解析過程不嚴謹,可見仔細閱讀形式化表述的重要性。
5 架構設計體驗
5.1 架構成型過程
由於第一次作業就採用了拓展性強的遞迴下降方法,在本單元中,我基本上沒有需要重構的地方,可以直接針對需求增添新的功能模組。為了支援第二次作業,我增加了TriFunc
等新的因子類和MergeTerm
等新的合併類;為了支援第三次作業,我增加了TriExprFunc
因子類用於三角函式巢狀因子,架構的成型過程循序漸進、連續一致。
5.2 心得體會
由於我上學期修過《Java程式設計》一課,Java使用比較熟練,因此本次作業中語言使用方面的問題幾乎沒有出現。與之相對,思路上的問題則讓我覺得比較吃力。
第一次作業中,考慮到後續迭代開發,我決定使用遞迴下降演算法。儘管課程組在課上實驗提供了遞迴下降的模板程式碼,我依然花費了較多時間才得以理解。而從理解到自己能熟練使用,又需要更深入地學習。不過,由於第一次作業整體難度較低,最終還是較順利地完成了。為了保證作業的正確性,我用剩餘的時間寫了一個評測機,較嚴謹地評測了自己的程式碼,最終在強測中取得滿分。第一次作業的順利完成很大地增長了我的信心。
第二次作業中,我遇到的難點在於深拷貝和equals()方法重寫。由於第二次作業不再限制括號深度,我的程式碼不可避免地需要使用深拷貝;同時,為了合併同類項,又不得不重寫equals()方法。為了解決這兩個問題,我搜了許多網上的資料,學習了討論區和助教們提供的許多方法,最終得以解決這兩個問題。第二次作業給我帶來的收穫就在於提高了自己蒐集資料,解決問題的能力。
第三次作業中,我在程式碼實現上基本沒有遇到阻礙,很快就完成了迭代開發,但卻被一個弱測點困擾了很久。這次作業的debug時間尤其漫長,我使用了各種方法,包括逐字逐句與第二次作業對比、使用各類自己的樣例測試,最後使用評測機測試都沒能解決問題。無奈之下只得自己重新一點一點看指導書和程式碼。最後發現問題出在形式化表述漏看了一個空白項。這次作業再次提醒我審題的重要性。
總而言之,OO第一次作業中,我收穫了許多,也取得了不錯的成績。希望能在第二單元再接再厲!