2022-OO-Unit1
2022-OO-Unit1
mashiroly
1. 引子
在供參考的部落格要求中,可以體會到課程組呼籲同學們“將關注點置於已完成的程式碼”,分析其結構與問題。本文當然不會缺失這一部分,但本文更願意將重點放在“設計”,縷清這一個月來認識“面向物件”的思路變化。
2. 設計
先簡述迭代過程。
hw1:在題目要求上增加可處理多層括號;
hw2:在題目要求上增加可處理三角函式巢狀因子;
hw3:更新自定義函式巢狀。
2.1 遞迴下降與統一表示
遞進式的三次作業中,架構的整體思路是一致的,即歸納為“遞迴下降”與“統一表示”。hw1已經確定了思路,也因此沒有經歷大規模重構。因此,本小節不強調三次作業題目的區別,大部分直接對hw3的題目表述進行分析。
對個人而言,這也產生了新的問題,眼高手低意味著第一週工作量極大。
2.1.1 遞迴下降:從形式化表述到結構化資料
flowchart TB A[Expr] --> B[Term] B -->C[Factor] C --- D[Num] C --- E[Pow] C --- F[ExprFactor] F -.ONE.-> A C --- G[Tri] C --- H[SelfFunc] C --- I[Sum] G -.TWO.-> C H -.THREE.-> C 上圖為根據形式化表述簡化的表示式結構。根據形式化表述,Expr、Term與Factor天然存在層次關係與遞迴定義。我們根據上圖的方式建立句法分析類Lexer、解析類Parser,將表示式的解析劃分為三層,按字讀取、正則表示式輔助、順序地逐層解析、返回下一層的結果。
在Factor一層,我們遇到了ExprFactor、Tri和SelfFunc三種涉及遞迴的Factor,具體實現用一句話概括,就是“左括號遞迴,右括號返回”。實現ExprFactor到Expr的遞迴,我們就可以處理多層括號巢狀。實現Tri和SelfFunc到Factor的遞迴(下一步往往是再次把解析任務交給ExprFactor),我們就可以處理這兩種函式的巢狀。
這時,我們會遇到一個問題:現在我們的確能將表示式解析成樹狀結構了,但我們最終目的是對錶達式展開化簡——所謂“返回下一層的結果”,返回的是什麼呢?為了構建起樹狀結構,為了父節點能夠”看見“子節點,這個”結果“理應是物件本身,並把子節點物件加入父節點物件的屬性。但是,子節點物件所表示的資料
這個問題直接催生了“統一表示”的思路,並使得設計和實現緊密關聯。
2.1.2 統一表示:化程式設計師所見為程式之所見
對於程式設計師而言,不論是4
,還是(x+1)
,還是(sin((+-x**2-1)**3))**4-+-3*cos(0)
,甚至是不合法的++--+123
,都是人腦可理解、可計算的“算式”,這是因為人腦完全理解“算式”的計算順序、計算規則,並能夠完全看見“算式”的全部資訊並同時利用。
但對程式而言,句法分析類Lexer只能看見當前解析到的位置,物件只能看見物件本身,可以訪問子節點物件(因為已經包含在屬性中)。如果不能做好資料的管理,那麼所謂“物件協同”僅止於構建一棵樹。
站在程式設計師的角度,既然不論何種“算式”都共用同一套計算規則,我們就應該建立一套完備的統一表示,讓各級物件的資料可以互動。統一表示成什麼因人而異,重要的是有其思想。在我的設計中,建立Basic類:
public class Basic {
private BigInteger coef = BigInteger.ONE;
private BigInteger pow = BigInteger.ZERO;
private HashMap<Factor, BigInteger> sin = new HashMap<>();
private HashMap<Factor, BigInteger> cos = new HashMap<>();
}
完備地將樹中處於葉結點Factor的資料表示為$$coefx**pow\Pi(\sin(Factor)a*cos(Factor)b)$$,例如:1
,x**2
,sin(x)**4
.如需拓展其他函式,只需修改Basic類即可。
Basic類不是葉結點Factor。(1+x)
顯然需要兩個Basic物件才能表示,因此需要容器basics管理Basic物件,並將容器作為表示該資料的物件的屬性。現在,我們可以將2.1.1節圖中最下一排(從Num到Tri)全部建類,唯一的屬性是basics。結合抽象層次關係,我們讓這些類統一實現Factor介面,運用歸一化的思想,建立歸一化訪問,無差別引用下層資料。對於SelfFunc和Sum,實踐中感覺使用Factor介面比較困難,因此按照2.3.4節的方法當作Expr處理。
在我的設計中,basics是HashMap<Basic, BigInteger>,其中BigInteger是Basic作為一個因子整體的冪次。如(2*x)**2 -> <{2,1,…},2>
我們終於解決了葉結點Factor如何儲存資料的問題,從最底層向上返回的物件終於可以將資料交給上層參與計算了。但統一表示還能更新一步:不論是Term還是Expr也能用basics的形式表示,因此將Expr和Term也實現Factor介面。顯然Expr和ExprFactor類是一致的,我們統一為Expr類。自此,任何一級合法表示式都能用basics表示,只需在類內填補構建、計算、優化的方法了。
2.2 搭建架構
classDiagram class Parser{ -lexer -funclist +Parser(String str, SelfFunclist funclist) +parseExpr() +parseTerm() +parseFactor() } class Lexer{ -cutToken -input -details -num -funcName +Lexer(String str) +reWriteINput +getCurToken() +getDetails() +getNum() +getFuncName() +next() } Parser <.. Lexer Parser <.. SelfFunclist class SelfFunclist{ -funcs +addFunc() +getFuncs() } SelfFunclist *-- SelfFunc class SelfFunc{ -funcList -name -expression -vars +define(String str) +setList() +reference() +getExpression() +getName() } class Sum{ -iter -cnt -start -end +Sum() +reference(String str) } class Factor{ <<interface>> +getBasics() +toString() +getExp() +setExp() +equals() } Factor<|..Num Num:-basics Num:-exp Num:+Num(BigInteger coef) Factor<|..Pow Pow:-basics Pow:-exp Pow:+Pow(BigInteger pow) Factor<|..Sin Sin:-basics Sin:-exp Sin:+Sin(Factor factor, BigInterger exp) Factor<|..Cos Cos:-basics Cos:-exp Cos:+Cos(Factor factor, BigInterger exp) Factor<|..Expr Expr:-basics Expr:-terms Expr:-exp Expr:+getTerms() Expr:+addTerm(Term term) Expr:+addBasics(Term term) Factor<|..Term Term:-basics Term:-factors Term:-exp Term:+addFactor(Factor factor) Term:multBasics(Factor factor) Term:Merge(HashMap basics) Basic ..>Factor Sum ..>Expr SelfFunc ..>Expr class Basic{ -coef -pow -cos -sin +Basic() +Basic(Basic) +multCoef() +addCoef() +addPow() +addSin() +addCos() +merges() +equals() +toString() } 整體思路已經在2.1兩小節清晰表述。Parser類和Lexer類協作,提取當前字元(串)的資料,並將資料分配給對應的物件;所有實現Factor介面的類都具有屬性Basic,只有讀到葉結點時,才會呼叫Basic中的方法儲存資料並返回。Term的方法實現了Factor之間的乘法與冪次,合併同類項,保證交給Expr的Basic冪次為1 且已合併同類項(出於HashMap容器的要求)。Expr的方法實現了Term之間的加法及合併同類項。最後通過toString直接將頂層Expr的basics屬性輸出。
2.3 難點問題
2.3.1 連續符號
連續符號對我而言是hw1的一道坎。最初採用符號計數器,但由於正則表示式的設計,遇到形如+-(...)
無法處理。換用思路:遇到負號則遞迴,返回時取相反。實現過程中發現很難確定什麼時候返回,bug頻出。最終採用了直接改寫原串為+1*-1*(...)
的形式,並控制pos回撥,順利解決。
2.3.2 冪次
對冪次的處理可能是最糾結的一步:到底給Term處理還是給Expr處理?最終,由於冪次與乘法的相似,將其交給Term,保證返回給Expr時整體的exp一定是1,更體現物件的內聚,也算多了一種debug的方案。
2.3.3 解析自定義函式與求和函式
由於感到困難(偷懶),並沒有對自定義函式與求和函式重新建樹處理,而是利用對括號建棧解析、字串替換得到新的Expr,重新建Parser、傳入函式表解析,這樣返回的仍是Expr,作為Factor,交給上級處理。
3. 度量分析
3.1 類的內聚與耦合
OCavg:平均操作複雜度。
OMax:最大操作複雜度。
WMC:加權操作複雜度。
class | OCavg | Ocmax | WMC |
---|---|---|---|
expression.Basic | 2.533333333333333 | 10.0 | 38.0 |
expression.Cos | 1.3333333333333333 | 3.0 | 8.0 |
expression.Expr | 1.5 | 4.0 | 15.0 |
expression.Num | 1.3333333333333333 | 3.0 | 8.0 |
expression.Pow | 1.3333333333333333 | 3.0 | 8.0 |
expression.SelfFunc | 2.142857142857143 | 6.0 | 15.0 |
expression.SelfFuncList | 1.0 | 1.0 | 3.0 |
expression.Sin | 1.3333333333333333 | 3.0 | 8.0 |
expression.Sum | 1.6666666666666667 | 3.0 | 5.0 |
expression.Term | 2.4444444444444446 | 9.0 | 22.0 |
MainClass | 1.5 | 2.0 | 3.0 |
parser.Lexer | 2.8 | 11.0 | 28.0 |
parser.Parser | 3.6 | 11.0 | 18.0 |
Total | 184.0 | ||
Average | 1.978494623655914 | 5.0 | 13.142857142857142 |
兩個解析類的複雜度較高,這是遞迴下降本身的因素。而資料類中Term複雜度極高,歸因為乘法和冪次依賴多次遍歷,未能找到更好的方案。
3.2 方法的複雜度
CogC:認知複雜度。
ev(G):基本圈複雜度。
iv(G):設計複雜度。
v(G):圈複雜度。
選擇複雜度最高的方法列出。
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
expression.Basic.addCos(Factor, BigInteger) | 9.0 | 4.0 | 5.0 | 5.0 |
expression.Basic.addSin(Factor, BigInteger) | 9.0 | 4.0 | 5.0 | 5.0 |
expression.Basic.equals(Object) | 4.0 | 3.0 | 5.0 | 7.0 |
expression.Term.Merge(HashMap, HashMap) | 8.0 | 4.0 | 5.0 | 5.0 |
expression.SelfFunc.reference(String) | 8.0 | 1.0 | 7.0 | 7.0 |
expression.Term.multBasics(Factor) | 24.0 | 1.0 | 9.0 | 9.0 |
expression.Basic.toString() | 18.0 | 2.0 | 10.0 | 10.0 |
parser.Lexer.next() | 13.0 | 2.0 | 10.0 | 11.0 |
Total | 146.0 | 117.0 | 172.0 | 191.0 |
Average | 1.5698924731182795 | 1.2580645161290323 | 1.8494623655913978 | 2.053763440860215 |
就平均複雜度而言,各方法還算比較簡單。複雜度最高的不出意料仍是multBasics。此外,由於沒用單獨的優化類,優化完全通過toString內部的分支實現,分支複雜。
4. 測試與bug分析
測試主要採用自動評測與手動構造邊界資料結合。由於沒用自己動手實現評測機,這裡描述構造和互測出的bug。
- 連續符號,如2.3.1所述;
- 自定義函式和求和函式”字串替換實現,遺漏了需要加括號的情況。
總之,bug均出自較“面向過程”的步驟,所在模組複雜度均較高,企圖採用trick避免對結構的設計。在之後的作業中應直面問題,訓練的目的在“學會“而非”答題“。