BUAA_OO_2022第一單元總結
一、前言
本單元的主題為表示式的識別與化簡。個人認為本單元的作業難度相比Pre和先前編寫的程式碼作業難度和工程量有著明顯的提升,此外,還面臨著面向物件思想的轉變。這一度在開始時讓我手足無措。多虧第一單元訓練給我指明瞭方向並讓我理解了遞迴下降的思想。
2.1.第一次作業
UML類圖如下所示:
第一次作業主要參考了第一單元訓練的架構,將詞法分析和轉換用兩個類Lexer和Parser表示,使用遞迴下降構建表示式資料結構。
程式主要思想:
-
表示式預處理
預處理將去掉讀取表示式的所有空白符,並使用正則表示式化簡所有相鄰的正負號。
-
表示式解析——遞迴下降
在Parser類中遞迴處理表達式的識別工作,結構為
parseExpr(), parseTerm(), parseFactor()
parseFactor()
中,當識別到括號時,建立表示式因子,並遞迴呼叫表示式的parse操作parseExpr()
. -
表示式構建
根據形式化表述,使用Expr, Term, Factor三個層次依次表示表示式、項、因子。每一級使用HashSet儲存其所包含的下一級結構。PowerFactor, ConstantFactor, Expr三個類均實現了Factor介面。
-
化簡時的儲存結構
由於第一次作業中的基本因子僅有冪函式因子和常數因子,最終化簡所得的表示式必然是一個x的多項式。故使用一個HashMap<BigInteger, BigInteger>來做表示式的化簡儲存結構,其中key為x的指數,value為x^{key}項的係數。
程式碼共460行
主要高複雜度方法如下:
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
MyHashMap.toString() | 24 | 2 | 11 | 12 |
Expr.constructTermMap(Term) | 17 | 1 | 7 | 7 |
Parser.parseFactor() | 15 | 3 | 8 | 8 |
Lexer.next() | 11 | 2 | 7 | 10 |
Expr.polyMul(MyHashMap<BigInteger, BigInteger>, MyHashMap<BigInteger, BigInteger>) | 9 | 3 | 4 | 6 |
類複雜度如下:
Class |
OCavg | OCmax | WMC |
---|---|---|---|
MyHashMap | 5.5 | 10 | 11 |
Parser | 4 | 8 | 16 |
Expr | 3.33 | 7 | 20 |
Lexer | 2.57 | 8 | 18 |
MainClass | 2 | 2 | 2 |
PowerFactor | 2 | 3 | 4 |
ConstantFactor | 1.4 | 3 | 7 |
Term | 1.4 | 3 | 7 |
高複雜度原因分析:
-
我在輸出時,用MyHashMap類繼承HashMap類,並複寫toString方法。由於優化引入了大量if else巢狀判斷語句,導致複雜度較大,從而直接導致了MyHashMap類的高複雜度。
-
另外,constructTermMap方法作為簡化表示式的核心方法,由於未充分拆分導致程式碼較為冗長,複雜度較大。
-
Parser類由於表示式的轉化存在遞迴結構和大量判斷語句,耦合度較大。
2.2.第二次作業
UML類圖如下所示:
第二次作業增添了三角因子、自定義函式和求和函式三個因子,程式結構相比第一次作業主要的改動如下:
-
增添TriFactor類
和PowerFactor類似定義了三角因子,繼承Factor父類。內部屬性factor利用Factor類實現,此處並未侷限於第二次作業三角函式內部僅有冪函式因子和常數因子的限定,為第三次作業的擴充套件打下基礎。
-
增添FuncProcessor類
本類整合了讀入表示式對自定義函式和求和函式做預處理的一些方法。我使用正則表示式識別加字串替換的方法預先將讀入的表示式進行處理,識別內部所有的自定義函式和求和函式並進行替換,再交給Lexer, Parser進行解析。
這樣的預處理方法存在明顯的侷限性,也就是函式巢狀時難以識別到正確的括號匹配,在第三次作業中對此做了修復。
-
採用巢狀的HashMap結構化簡表示式
由於三角因子的加入,化簡表示式時無法再視為多項式進行處理,故使用
HashMap<HashMap<String, BigInteger>, BigInteger>
這樣的巢狀HashMap進行表示式的化簡。其中外層HashMap的key為表示式的項,value為該項的係數;內層HashMap的key為項內因子的字串形式,value為該因子的冪次。例如,3*sin(x)*x**2+2*cos(x)的expMap為:
{ ( {("sin(x)", 1), ("x", 2)} , 3), ( {("cos(x)", 1)} , 2) }.
另外,由於項Term和表示式Expr具有相似的結構,項內可能有表示式因子,表示式內部也有很多項,所以項和表示式在化簡時是相互耦合的。故將項和表示式均構造成上述的HashMap進行統一處理,具體實現為Term.constructTerm()和Expr.constructExpr()相互巢狀。
程式碼共832行
主要高複雜度方法如下:
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Expr.output(HashMap<HashMap<String, BigInteger>, BigInteger>) | 50 | 2 | 17 | 17 |
Term.constructTerm() | 39 | 1 | 14 | 14 |
FuncProcessor.replaceFunc2(String, String, String) | 30 | 12 | 11 | 14 |
FuncProcessor.replaceSum(String) | 18 | 9 | 8 | 9 |
Term.mapMul(HashMap<HashMap<String, BigInteger>, BigInteger>, HashMap<HashMap<String, BigInteger>, BigInteger>) | 17 | 3 | 6 | 8 |
Parser.parseFactor() | 16 | 4 | 11 | 11 |
Lexer.next() | 15 | 2 | 11 | 14 |
FuncProcessor.parseFunc(String, String, String, String, String, String, String, ...) | 10 | 1 | 6 | 6 |
Lexer.getTriFactor(String) | 8 | 1 | 6 | 8 |
類複雜度如下:
Class | OCavg | OCmax | WMC |
---|---|---|---|
FuncProcessor | 5.86 | 14 | 41 |
Term | 5.17 | 14 | 31 |
Parser | 4 | 8 | 16 |
Expr | 3.5 | 14 | 28 |
Lexer | 3.43 | 10 | 24 |
TriFactor | 3.33 | 5 | 10 |
MainClass | 2 | 2 | 4 |
PowerFactor | 1.75 | 3 | 7 |
ConstantFactor | 1.33 | 3 | 8 |
Factor | 0 |
與第一次作業對比可發現,由於題目變得更加複雜,我原本的架構由於對耦合的優化不夠,程式碼複雜度大量膨脹。對這部分的優化主要在第三次作業中進行。
本次作業的高複雜度方法主要集中於化簡輸出和最初預處理自定義函式和求和函式兩個模組。Term.constructTerm()由於其對不同型別Factor的分類和大量的HashMap操作使得複雜度很高。Expr.output同樣由於優化複雜度大大提高。
2.3第三次作業
UML類圖如下所示:
第三次作業增加了表示式的多層巢狀和函式呼叫的多層巢狀,三角因子的內部也支援所有的因子。程式結構相比第二次作業,主要改動如下:
-
調整Processor類
將函式和求和函式的預處理分開成兩個類以降低類複雜度。在自定義函式的替換時,與第二次作業正則表示式匹配整個函式呼叫不同,為了處理巢狀呼叫,本次僅使用正則表示式匹配函式名f, g, h. 在函式呼叫引數的解析方面,使用堆疊原理尋找與之匹配的左右括號以及內部的每一個逗號,最終進行字串替換。此方法的思維量較低,但工程量較大,最終使得此模組的相關方法複雜度較大。
-
TriFactor內部factor的解析
把TriFactor的括號內部當成一個一般因子處理,建立新的Lexer和Parser,呼叫parseFactor()方法進行解析,以解決三角巢狀問題。此外,在內部為表示式因子的toString方法中呼叫了Expr.output()方法生成所需的字串作為外層表示式內層HashMap的鍵。
程式碼共965行
主要高複雜度方法如下:
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
FuncProcessor.processFuncCall(String, String, int, String, String, String, String) | 31 | 12 | 14 | 15 |
SumProcessor.replaceSum(String) | 21 | 9 | 9 | 10 |
Term.mapMul(HashMap<HashMap<String, BigInteger>, BigInteger>, HashMap<HashMap<String, BigInteger>, BigInteger>) | 17 | 3 | 6 | 8 |
Expr.processOutEntry(StringBuilder, Entry<HashMap<String, BigInteger>, BigInteger>) | 15 | 1 | 7 | 7 |
Lexer.next() | 15 | 2 | 11 | 14 |
Expr.processInEntry(StringBuilder, Entry<HashMap<String, BigInteger>, BigInteger>) | 10 | 1 | 8 | 8 |
FuncProcessor.parseFunc(String, String, String, String, String, String, String, ...) | 10 | 1 | 6 | 6 |
類複雜度如下:
Class | OCavg | OCmax | WMC |
---|---|---|---|
SumProcessor | 6.5 | 10 | 13 |
FuncProcessor | 5 | 12 | 30 |
Processor | 4 | 4 | 4 |
Term | 3.55 | 8 | 39 |
Lexer | 3.43 | 10 | 24 |
Parser | 3.4 | 6 | 17 |
Expr | 3.18 | 6 | 35 |
TriFactor | 2.67 | 6 | 16 |
MainClass | 2 | 2 | 4 |
PowerFactor | 1.75 | 3 | 7 |
ConstantFactor | 1.25 | 3 | 10 |
Factor | 0 |
本次作業做了大量的方法拆分和功能再整理,使得整體的方法複雜度有所下降,最明顯的即為Expr.output(), 在將其拆分為Expr.processInEntry()和Expr.processOutEntry()後複雜度大大降低。但由於預處理求和函式和自定義函式的演算法較為複雜導致SumProcessor和FuncProcessor及其相關方法的複雜度仍居高不下。
三、優缺點
3.1優點
-
由於分三層次遞迴下降解析的總體架構在第一次作業中已明確,在迭代開發的過程中未進行大規模重構。基本思想一脈相承,迭代時不必推倒重來。
3.2缺點
-
未做充分的化簡,例如在第二次作業中未做cos(0)類的優化,第三次作業未做倍角公式優化導致效能分缺失。
-
自定義函式及求和函式的字串替換方法複雜度較大,可迭代性不足,對自定義函式和求和函式建模的方法雖然更佳,由於作業時間吃緊並未採用,略顯遺憾。
四、主要Bug以及互測策略
4.1主要bug
由於測試不夠充分,我的bug大部分為低階錯誤,在今後的作業中需要多加改正,多做測試。
第一次作業中未考慮指數前的的‘+’
第三次作業中對於sin(0)**0特判順序錯誤導致輸出0,還有三角因子toString方法對於sin(cos(x)**2)這樣的輸入中指數判斷有誤。
本單元作業的bug主要還是出在化簡和輸出階段。這可能與此過程方法複雜度大有一定相關。
4.2互測
互測時我採取的是選取易錯資料手動測試的方法,由於優化過程容易出現錯誤,故在互測中重點檢視優化部分的程式碼(例如將“1*"直接替換為空串的暴力優化[捂臉])。
此外,還構造了邊界資料,例如構造上下限均超int範圍的求和函式和大數的冪等。
由於我每次hack均是已在本地測到bug才上傳,故準確率較高,但由於資料範圍有限,覆蓋面不夠廣。
五、架構設計體驗
在第一週的開發時,大量的技術細節和複雜的指導書表述讓我一度迷失在文字的叢林,思路較為混亂。在做完訓練後,學到了遞迴處理的精妙之處後,我腦海裡的架構才逐漸成型。
本單元充分地讓我體會到了架構設計的重要性。在確定好一個好的架構以後,再去迭代開發,每一步都比較清晰明確。但儘管如此,在實際編寫程式時,我仍然感覺自己的架構還是沒能很好體現OO的封裝思想,甚至有些部分有些醜陋。但本次的架構設計過程的確讓我在不斷迭代中思路一次一次更加清晰。
六、心得體會