BUAA-OO-unit-1-總結
BUAA-OO-unit-1-總結
目錄第一單元主題為對錶達式結構進行建模,完成表示式的括號展開與化簡。主要學習目標是熟悉面向物件思想和原則,熟悉層次化設計的思想。在第一單元結束之際,對自己的設計思路進行總結,分享心得體會。
第一次作業
程式碼結構分析
思路與設計依據
第一次作業為簡單表示式的建模與去括號、化簡。在設計架構的時候,並沒有因為單層括號巢狀等約束條件去簡化架構,而是充分考慮了架構的可拓展性。
-
將程式碼分為互不耦合的兩部分:解析與表示式層次結構。
- 解析部分中,先利用tokenizer進行詞元化(如,
x**2*-2*(12+x+2*x**2)**2
->[x, **, 2, *, -, 2, *, (, 12, +, x, +, 2, *, x, **, 2, ), **, 2]
),然後利用遞迴下降的parser構建層次結構。Tokentype
類利用enum
儲存詞元型別。 - 利用依賴倒轉原則,對於factor,分別新增底數
Item
exponent
屬性,讓常數因子、冪函式因子、表示式因子等都繼承自Item
類。(常數因子的exponent設定為0即可)
- 解析部分中,先利用tokenizer進行詞元化(如,
-
拆括號:定義表示式,項,因子的加法與乘法,對於表示式因子的相乘與冪,按照基準思路展開計算。
-
深度合併同類項:在Term中設定了係數和餘項屬性。合併時會利用HashMap,同時遞迴地重寫quals方法,合併時會dfs比較餘項。這麼做是考慮到了後續出現三角函式,需要合併
2*sin(1+x)+3*sin(x+1)
這種情況,因此具體在第二次作業的section中討論。
uml類圖
classDiagram class Calculatable{ <<interface>> } Calculatable <|.. Term Calculatable <|.. Expr Calculatable <|.. Factor Calculatable <|.. Item Calculatable :void + simplify(int) Calculatable :boolean + equals(Object) Calculatable :String + display() class Item { -int signedness +Item(int) +simplify() +display() } class Factor { -Item element; -Const exponent; +Factor(Item element, Const exponent) +simplify(int dep) +deepClone() } class Term{ -int signedness; -boolean ifNeedRemoveParen; -HashSet<Factor> powers -Const coe; -HashMap<Item, Const> hashFactors +Term(HashSet<Factor> powers, Const coe) +Term(ArrayList<Factor> factors, int signedness) +addFactor(Factor factor) +String display() +simplify(int dep) +mergeFactors() +mul(Term obj) +ArrayList<Factor> flatten() +removeParen() +toCeoPowers() } class Expr { -ArrayList<Term> terms -HashMap<HashSet<Factor>, Const> hashTerms +Expr(ArrayList<Term> terms) +addAll(Expr obj) +mul(Expr obj) +replace(HashMap<Var, Factor> table) +simplify(int dep) } class ExprFactor { -Expr expr +ExprFactor(Expr expr) } class Const { -BigInteger num +Const(int signedness, BigInteger num) +mul(Const obj) +add(Const obj) +pow(Const exp) } class Var{ -String varName +Var() +Var(String varName) } class Tokenizer{ -ArrayList<Token> tokens -String expression +ArrayList<Token> getTokens() -isWhitespace(char c) } class Token { -BigInteger num; -TokenType tokenType; -String name; +Token(BigInteger num, TokenType tokenType) +Token(String name, TokenType tokenType) +Token(TokenType tokenType) +String toString() } class Parser{ -ArrayList<Token> tokens -int tokenIdx +Parser(ArrayList<Token> tokens) +parseExpr() Expr +parseTerm() Term +parseFactor() Factor +parseConst() Const } class MainClass Item <|-- ExprFactor Item <|-- Const Item <|-- Var Token <.. Tokenizer Tokenizer <.. Parser Parser <.. MainClass Expr <.. MainClass基於度量的程式碼分析以及優缺點
-
方法複雜度
method CogC ev(G) iv(G) v(G) Tokenizer.getTokens() 28.0 15.0 21.0 21.0 Term.display() 20.0 1.0 6.0 10.0 Parser.parseFactor() 16.0 6.0 7.0 10.0 Parser.parseExpr() 10.0 4.0 6.0 7.0 Term.toCeoPowers() 9.0 1.0 7.0 8.0 Parser.parseTerm() 8.0 4.0 8.0 9.0 Term.numOfExprFactor() 7.0 1.0 7.0 7.0 Term.removeParen() 5.0 1.0 4.0 4.0 -
類複雜度
class OCavg OCmax WMC Const 1.4545454545454546 3.0 16.0 Expr 2.272727272727273 7.0 25.0 ExprFactor 1.3333333333333333 3.0 8.0 Factor 1.4285714285714286 3.0 10.0 Item 1.3333333333333333 3.0 8.0 MainClass 1.0 1.0 1.0 Parser 5.0 8.0 25.0 Term 3.0555555555555554 9.0 55.0 Token 4.0 13.0 16.0 Tokenizer 6.333333333333333 16.0 19.0 Var 1.0 1.0 4.0 -
類關聯矩陣
-
優點:對類與方法複雜度進行分析,可以發現複雜度較高的方法主要集中在詞元化,解析這些方面,會出現較多分支結構。其餘部分方法複雜度較低,基本做到了高內聚,低耦合。
-
缺點:由於考慮到後續作業,程式碼長度較長。對於第一次表示式內部的關聯矩陣來看,項和表示式,常數和項,因子和項的關聯度很高,這是因為,出現了迴圈依賴的情況;且由於將項提取了係數,導致常數與項依賴較高。由此可見,在表示式和項的層面,還沒有充分應用依賴倒轉原則,層次安排較不靈活。
測試思路與策略
-
評測機的開發
-
第一次作業,利用python的popen與sympy實現了評測機的構建。
-
評測機分為多專案自動編譯,自動評測兩部分。
-
使用subprocess,自動尋找主類,編譯加入FILELIST,利用檔案list實現多專案的編譯,便於互測使用。
-
主要評測邏輯:
-
測試得到答案
os.chdir(DIR) x = sympy.Symbol('x') #try: myAnswer = os.popen('echo ' + fx + '|java ' + NAME) myAnswer = myAnswer.read().split("\n")[0]
-
判定正誤
tureValue = sympy.sympify(sympy.expand(fx)) myValue = sympy.sympify(myAnswer) if myValue == tureValue: #output else: #output
-
-
-
資料生成策略
- 按照遞迴下降生成即可,比較簡單。注意,在dfs時應記錄深度與冪次,防止非法。
- 同時可以利用常量池生產常數,藉此進行cornercase、極限情況的測試。
bug與評測分析
- 在互測與強測中未出現bug。
- hack的策略為隨機資料+縮小範圍。利用隨機資料發現bug,再手動縮小bug範圍。兩位同學的bug,一位在處理
+1*x
時出現了輸出1x
的問題,另一位在處理前導零時出現了問題。
第二、三次作業
由於第二、三次作業的程式碼改動不足十行,因此放在一起敘述。
程式碼結構分析
UML類圖與架構
-
第二次作業在第一次的基礎上增加了三角函式,求和函式,以及自定義函式呼叫,難點主要集中在變數-因子的替換,添加了三角函式後如何深度合併同類項,三角優化這幾個方面。
-
全部程式碼UML類圖如下:
classDiagram class Calculatable{ <<interface>> } Calculatable <|.. Term Calculatable <|.. Expr Calculatable <|.. Factor Calculatable <|.. Item Calculatable :void + simplify(int) Calculatable :boolean + equals(Object) Calculatable :String + display() Calculatable :replace(HashMap<Var, Factor> table) class Item { -int signedness +Item(int) +replace() +simplify() +display() } class Factor { -Item element; -Const exponent; +Factor(Item element, Const exponent) +simplify(int dep) +replace(HashMap<Var, Factor> table) +deepClone() } class Term{ -int signedness; -boolean ifNeedRemoveParen; -HashSet<Factor> powers -Const coe; -HashMap<Item, Const> hashFactors +Term(HashSet<Factor> powers, Const coe) +Term(ArrayList<Factor> factors, int signedness) +addFactor(Factor factor) +String display() +simplify(int dep) +mergeFactors() +mul(Term obj) +ArrayList<Factor> flatten() +removeParen() +toCeoPowers() +replace(HashMap<Var, Factor> table) } class Expr { -ArrayList<Term> terms -HashMap<HashSet<Factor>, Const> hashTerms +Expr(ArrayList<Term> terms) +addAll(Expr obj) +mul(Expr obj) +replace(HashMap<Var, Factor> table) +simplify(int dep) } class Sin { -Factor factor +Sin(Factor factor) } class Cos { -Factor factor +Cos(Factor factor) } class ExprFactor { -Expr expr +ExprFactor(Expr expr) } class Const { -BigInteger num +Const(int signedness, BigInteger num) +mul(Const obj) +add(Const obj) +pow(Const exp) } class Var{ -String varName +Var() +Var(String varName) } class Tokenizer{ -ArrayList<Token> tokens -String expression +ArrayList<Token> getTokens() -isWhitespace(char c) } class Token { -BigInteger num; -TokenType tokenType; -String name; +Token(BigInteger num, TokenType tokenType) +Token(String name, TokenType tokenType) +Token(TokenType tokenType) +String toString() } class Parser{ -ArrayList<Token> tokens -HashMap<String, Func> functions -int tokenIdx +Parser(ArrayList<Token> tokens, HashMap<String, Func> functions) +Parser(ArrayList<Token> tokens) +parseExpr() Expr +parseTerm() Term +parseFactor() Factor +parseSum() Factor +parseFun() Factor +parseTriangle(TokenType tokenType) Factor +parseConst() Const } class Func{ -ArrayList<Var> vars -String funcName -Expr expr +parse(String definition) +deepClone() Func } class MainClass Item <|-- Sin Item <|-- Cos Item <|-- ExprFactor Item <|-- Const Item <|-- Var Parser <.. MainClass Expr <.. MainClass Token <.. Parser Func <.. Parser Expr <.. Parser Tokenizer <.. Token -
架構
- 沿襲了第一次作業的架構,分為不互相耦合的兩部分:解析部分和表示式層次結構。
- 解析部分中,先利用tokenizer進行詞元化,然後利用遞迴下降的parser構建層次結構。
Tokentype
類利用enum
儲存詞元型別。 - 對於factor,分別新增底數
Item
,指數exponent
屬性,讓常數因子、冪函式因子、三角函式因子、表示式因子等都繼承自Item
類。(常數因子的exponent設定為0即可)
基於度量的程式碼分析以及優缺點
-
類複雜度
class OCavg OCmax WMC Const 1.5 3.0 18.0 Cos 1.5714285714285714 4.0 11.0 Expr 2.3636363636363638 7.0 26.0 ExprFactor 1.2857142857142858 3.0 9.0 Factor 2.0 5.0 18.0 Func 2.1666666666666665 8.0 13.0 Item 1.2857142857142858 3.0 9.0 MainClass 2.0 2.0 2.0 Parser 5.333333333333333 12.0 48.0 Sin 1.5714285714285714 4.0 11.0 Term 2.75 9.0 55.0 Token 3.4285714285714284 18.0 24.0 Tokenizer 7.666666666666667 20.0 23.0 TokenType 0 0 0.0 Var 1.5 4.0 9.0 Average 2.4642857142857144 7.285714285714286 18.4 -
方法複雜度(這裡只列出複雜度較高的方法)
method CogC ev(G) iv(G) v(G) Tokenizer.getTokens() 35.0 19.0 31.0 31.0 Term.display() 20.0 1.0 6.0 10.0 Parser.parseTriangle(TokenType) 17.0 4.0 4.0 6.0 Func.parse(String) 16.0 7.0 10.0 12.0 Parser.parseFactor() 16.0 7.0 10.0 14. Parser.parseSum() 11.0 6.0 6.0 11.0 Parser.parseExpr() 10.0 4.0 6.0 7.0 Parser.parseFun() 10.0 6.0 3.0 6.0 Term.toCeoPowers() 7.0 1.0 4.0 5.0 Parser.parseTerm() 6.0 3.0 4.0 5.0 Term.numOfExprFactor() 5.0 1.0 5.0 5.0 Term.removeParen() 5.0 1.0 4.0 4.0 -
關聯矩陣
-
本次作業程式碼較長,有效行數約有1200行,但是每個方法行數基本控制在50行以內。
-
優點:對類與方法複雜度進行分析,可以發現複雜度較高的方法主要集中在詞元化,解析這些方面,會出現較多分支結構。其餘部分方法複雜度較低,基本做到了高內聚,低耦合。加入了三角函式類以後,耦合程度、複雜程度並沒有明顯提高,可見架構可拓展性很好。
-
缺點:表示式內部的關聯矩陣來看,項和表示式,常數和項,因子和項的關聯度很高,這是因為,出現了迴圈依賴的情況;且由於將項提取了係數,導致常數與項依賴較高。這是因為在表示式和項的層面,還沒有充分應用依賴倒轉原則,層次安排較不靈活。
設計依據與難點思路
-
深度合併同類項
- 我在第一次作業中就利用HashMap實現了深度合併同類項,分別在Term、Expr類中實現了相應的合併方法,因此此次作業改動較小。
-
事實上,Term、Expr中合併的方法是非常相似的,Term中對因子的合併主要依賴
HashMap<Item, Const> hashFactors
,Key為Factor的底數,Value為指數。Expr中對項的合併主要依賴HashMap<HashSet<Factor>, Const> hashTerms
,Key為用HashSet表示的項的除去係數的部分,Value為係數。-
當發生hash衝突,需要合併的時候,Term的指數相加,Expr的係數相加。
-
舉個例子:假設在Expr中,已有一個
ArrayList<Term> terms
,建立HashMap
的時候可以利用Collectors.toMap()
方法,在key重複的時候,可以利用Const.add()
方法。hashTerms = terms.stream().collect(Collectors. toMap(Term::getPowers, Term::getCoe, Const::add, HashMap::new));
-
-
- 重寫
equals()
方法,利用dfs進行餘項的比較。注意應比較HashSet等無序容器,從而可以合併2*sin(1+x)+3*sin(x+1)
這種情況 - 出現三角函式時無需特殊處理,直接HashMap合併即可。
-
變數-因子的替換
- 求和函式和自定義函式呼叫的核心都是變數-因子的替換。並沒有使用正則表示式等字串替換策略;而是按層次結構建立自定義函式類,在遞迴下降解析的過程中對sum,functions替換。
-
在解析的時候,遇到需要替換的情況就立刻替換。
-
在所有表示式結構相關類中遞迴地實現Replace方法,要替換是傳入
HashMap<Var, Factor>
作為引數,key是變數類,表示自定義函式中待替換變數,Value為因子類,表示要替換成的因子。 -
在Factor類中實現對錶達式的更改,例如,對
y**2
做替換y->x**3
時,先合併指數,再替換底數。對替換成常數的情況需要特判,進行乘方運算,防止常數出現指數。例如,對y**2
做替換y->3
時,結果為9
。
-
深克隆:重複呼叫自定義函式,求和函式對因子的重複替換
-
先讓相應類實現
Serializable
序列化介面 -
然後利用位元組序列輸出流將物件序列化成位元組序列,將要序列化的物件寫入
-
再利用物件輸入流傳入位元組序列,反序列化到一個新物件裡就實現了深拷貝。
-
public Factor deepClone() { Factor res = null; try { ByteArrayOutputStream bo = new ByteArrayOutputStream(); ObjectOutputStream oo = new ObjectOutputStream(bo); oo.writeObject(this); oo.close(); ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray()); ObjectInputStream oi = new ObjectInputStream(bi); res = (Factor) oi.readObject(); oi.close(); } catch (Exception e) { System.out.println(e.getMessage()); } return res; }
-
注意:一定要讓待拷貝物件屬性中所有涉及的類都實現
Serializable
序列化介面,否則無法正常拷貝
-
優化策略
著重考慮 $$ sin→cos$$,$$ sin→cos$$ 的情況。
- 可以按照正弦餘弦建立兩個
HashMap
,將表示式中含有sin(x)**2
和含有cos(x)**2
到分別新增至HashMap<HashSet<Factor>, Const>
中,尋找兩個HashMap
key相等的情形,進行合併。
資料生成,自動評測
- 寫完以後再分享
bug與評測分析
- 第二次作業
- 強側與互測做到了bugfree。
- 自測發現深克隆相關的bug,一個出現在三角函式化簡中,另一個出現在重複呼叫函式中。(由此可見不可變物件有其好處)。
- 第三次作業
- 互測強測同樣未出現bug
- 自測時發現一個bug,在處理
sin((1+x)**2)
時會輸出sin((1+x)**2)
。但是強測似乎並沒有考察這個點?