BUAA_OO_2022 Unit1 總結
1 架構設計思路分析
1.1 總體設計思路
1.2 第一次作業
在第一次作業中,核心任務有兩個:初步構建表示式樹的架構、實現基於遞迴下降法的表示式解析;
基於形式化表述,表示式可以拆成若干個項,項之間通過±連線;項可以拆成若干個因子,因子間通過*連線;因子又分為表示式因子、冪函式因子、常數因子三種;因此,我將頂層的表示式歸入表示式因子中,定義一個Factor介面。
基於遞迴下降法,通過Lexer類將字串拆分成若干個語義單元Token,再通過Parser讀取Token流返回相應的表示式元素;這裡Parser裡的parseFactor方法運用了工廠模式的思想,在建立因子時隱藏了對因子種類的分類邏輯,並且通過一個共同的介面Factor來指向新建立的因子物件。
類圖描述
度量分析
OCavg
= Average operation complexity(平均操作複雜度)
OCmax
= Maximum operation complexity(最大操作複雜度)
WMC
= Weighted method complexity(加權方法複雜度)
Class | OCavg |
OCmax |
WMC |
---|---|---|---|
ExprFactor | 3.7142857142857144 | 10.0 | 26.0 |
Lexer | 1.5714285714285714 | 4.0 | 11.0 |
MainClass | 1.0 | 1.0 | 2.0 |
NumFactor | 1.0 | 1.0 | 3.0 |
Parser | 3.1666666666666665 |
6.0 | 19.0 |
PowFactor | 1.6666666666666667 | 3.0 | 5.0 |
Term | 2.2857142857142856 | 7.0 | 16.0 |
Total | 82.0 |
由上表可見,遞迴下降解析導致Lexer和Parser類複雜度較高,而表示式化簡導致ExprFactor和Term的複雜度較高。
由於此次作業的表示式化簡方法在下次作業進行了重構,這裡省略第一次作業的方法複雜度分析。
1.3 第二次作業
概述
第二次作業的要求相對第一次作業有了很多擴充,我將類主要分為流程類、資料類、功能類:
-
流程類就是
MainClass
,作為入口;
-
功能類有負責遞迴下降解析的
Lexer,Parser
Operation
,在Operation
裡實現了對錶達式的乘法和加法,對錶達式合併同類項和對項合併同底數因子;
-
資料類沿襲第一次作業的架構,新增了
TriFactor
繼承Factor
,同時新增了Sum
和Function
處理求和函式和自定義函式。
細節
三角函式類
在處理三角函式因子時,我將sin和cos統一定義為TriFactor
類,並用屬性name
來區分,這樣的好處是可以實現對sin和cos的統一修改,同時更接近問題的本質(即在表示式化簡中,sin和cos僅僅只是名字不同而已,並不涉及運算性質的差異)。
與不少同學在TriFactor
類中定義ExprFactor
來表徵三角函式中巢狀的內容不同,我用了Factor
來表徵。這是基於兩點考慮,首先是形式化表述裡本身用的就是因子,其次是這樣天然就實現了判斷toString
時是否需要加括號,而不用像很多同學一樣做複雜的分類討論(還容易出錯)。
求和函式和自定義函式類
這兩個類非常相似,都是涉及到將因子代入到相應的求和表示式/自定義函式定義表示式中。具體實現有兩種路徑:
一種是先對因子進行解析、化簡、toString
後replace
掉原表示式中的x、y、z、i
,最後再對這個新的字串進行解析。
另一種是先對求和表示式/自定義函式定義進行解析,建立表示式樹。在代值時,先對代入的因子進行解析,然後遞迴之前建立的表示式樹,用代入因子替換表示式樹種的冪函式因子。這裡需要注意的是,替換時需要將代入因子clone
,以消除資料共享。
操作類
經過分析後,我發現合併同類項、項內同底數因子合併、表示式加法、表示式乘法其實都是對一些裝有Factor
和Term
的容器進行操作,因此我將這些方法抽出來合成一個方法類,便於在不同的資料類中呼叫;
合併同類項
合併同類項的本質是比較表示式中的項除係數外的部分是否相等,若相等則可以合併,這裡的合併是指將兩個相等的項的係數相加,因此關鍵是如何判斷相等。
由於項是由因子組成的,因此對項相等的判斷可以歸結為對項內每個因子是否相等的判斷。而因子中只要表示式因子和三角函式因子會巢狀其他項和因子,其他因子可以作為葉子節點直接比較其內部的基本屬性是否相同判斷相等。而三角函式因子的判斷其實最終也會歸結為對錶達式因子相等的判斷,因此問題轉化為對錶達式因子相等的判斷。
由於表示式時由項組成的,因此對錶達式相等的判斷可以歸結為對錶達式內每個項是否相等的判斷。
至此,遞迴比較相等的框架已構建完成,可以通過重寫equal
和hashCode
實現。為了避免項和因子的順序影響對相等的判斷,我們用無序容器HashSet
來管理和存放表示式中的項和項中的因子;
類圖描述
度量分析
類複雜度分析
由於ExprFactor
和Term
承載了表示式化簡的大部分功能,因此複雜度較高。Function則是由於代值時邏輯較為複雜,導致複雜度較高。
方法複雜度分析
CogC
:cognitive complexity 認知複雜度,表徵程式碼可讀性
ev(G)
:Essential cyclomatic complexity 基本圈複雜度
iv(G)
:Design complexity 設計複雜度
v(G)
:cyclonmatic complexity 圈複雜度
由上可知,絕大部分方法的複雜度都較為理想,整體上符合高內聚低耦合的思想。
ExprFactor
中的printCoef
由於要判斷項前係數省略,邏輯較為繁瑣;Operation
中的unpack
由於要判斷三角函式內因子的型別和狀態判斷是否可以拆包展開,邏輯較為繁瑣;Parser
中的parserFactor
由於運用了工廠模式,需要判斷因子的型別,邏輯較為繁瑣;TriFactor
中的toString
由於承擔了將sin(0), cos(0)
化為常數的功能,邏輯較為複雜;
1.4 第三次作業
由於在第二次作業時我已經考慮了三角函式內巢狀表示式因子、自定義函式和求和函式巢狀的情況,且遞迴下降法與表示式樹結構天然支援表示式因子巢狀的情形,因此第三次作業我只在第二次作業的基礎上加了幾處對clone的呼叫,其他幾乎沒有區別。
2 bug 分析和測試
2.1 第一次作業
自己的bug
在第一次作業中,我將表示式因子的指數在解析時處理,即將同一個表示式多次放進容器中,這導致了資料共享,在面對(x+1)**2
這種資料時會出錯。在自測時我發現了這個bug,並通過clone修改,但由於修改時未全面檢查,有一處修改漏掉了,導致在某些特殊的情況下仍然會出錯。
後來反思時,總結了至少3個導致錯誤的原因。第一是沒有深入理解類、物件和物件引用之間的關係,沒有考慮到資料共享;第二是在發現數據共享的問題後,只在一處做了修改後就基於測試,且測試通過後就沒有再檢查是否有其他地方需要做相同的修改;如果將同質化的程式碼抽象成單獨的方法就可以避免這樣的問題;第三是在寫資料生成器時,忘記考慮表示式因子帶指數的情況,導致生成的資料無法測試出上述bug;這反應了我沒有深刻理解形式化表述,沒有意識到資料的全覆蓋性的重要性。
別人的bug
有同學在處理指數前的-
時將-
放進了指數中運算,導致-+x**2
這類資料出錯;
2.2 第二次作業
自己的bug
無
別人的bug
有同學在結果為0時沒有輸出;
有同學在三角函式中如果為負數且後面代指數時會出錯;
有同學用正則表示式+replaceAll
化簡係數和指數為1的情況,在遇到11*x、x**11
這類結果時會出錯;
2.3 第三次作業
自己的bug
由於在TriFactor
的normalize
方法中未判斷三角函式內巢狀的是三角函式時需要對內層三角函式繼續normalize
的情況,導致在合併同類項時會出錯;
在第二次作業中TriFactor
的toString
方法遇到sin(0)會輸出(0),然後對第一次輸出的結果再進行一次解析化簡。由於第二次作業括號巢狀層數不多因此不會出錯,但到第三次作業時,可能遇到sin(sin(0))的情況,由於只進行了一次重解析化簡,最終會輸出(0),含有多餘的括號,格式非法。
後來反思時,總結這兩個錯誤都是由於迭代開發時未仔細檢查程式碼中每一處需要修改的地方,導致第二次作業不會暴露的問題在第三次作業中暴露,其實這兩個bug最終僅需要修改三行,本身並非是設計、邏輯上出的問題,而是細節上的疏忽。
此外還有我過於依賴自動測試,以為自動測試沒發現bug就沒有再去檢查閱讀程式碼;但事實上,第一個bug由於我在自動生成器中設定了最大遞迴層數maxLevel = 3
(這樣的目的是防止資料過大跑不出來),導致會產生bug的資料很難被生成出來;第二個bug則是由於測評機本身無法判斷多餘的括號(這也是我的疏漏,我之前認為只要python庫不報錯格式就沒問題,但這沒考慮到我們作業要求不能出現多餘的括號,但python庫即使有多餘的括號也不會報錯);
別人的bug
有同學sum無法處理超過int的情況;
有同學sum中出現指數時會出錯;
對比分析發現,出現了bug的方法在程式碼行和圈複雜度明顯高於未出現bug的方法,這是因為程式碼行和圈複雜度高的方法一般運用的是面向過程的思維,含有繁瑣的分類討論,容易因疏忽出錯。
2.4 測試方法
在測試方面,我和zkg同學合作,我做了資料自動生成和,他負責自動測試。
關於資料生成,我基於形式文法和遞迴下降思想,生成了完全符合作業格式要求且全覆蓋的資料;為了讓常數具有特點,採用了常量池,並設定了引數調節常數的規模;為了控制最終表示式展開的複雜度,我在巨集定義中設定了maxLevel控制遞迴的深度,設定了maxPow控制指數的範圍,設定了項中因子個數的上下界和表示式中項的個數的上下界,設定了是否允許出現空白符;
生成的一大難點在於自定義函式,我將自定義函式的定義式和呼叫式分開,其中呼叫式只需要生成隨機的因子即可。關於定義式,我首先隨機函式自變數個數,然後隨機函式自變數的種類,然後通過allowVar[4]控制生成的函式表示式中允許出現的變元名稱;在求和函式中,也可以用類似的方法生成只含i和x的求和表示式;
關於自定義函式定義中不能出現自定義函式呼叫,以及sum求和表示式中不能出現求和表示式和自定義函式的呼叫,我用了allowFunc和allowSum兩個全域性變數來控制。
2.5 發現bug的策略
主要通過黑箱測試和白箱測試相結合的方法,先通過自動測試工具初步判定是否存在bug,再通過二分法不斷縮小測試樣例的規模精確定位bug的範圍,最後通過閱讀程式碼找到bug的所在。再修改bug後,再通過迴歸測試檢驗是否仍有其他bug。
在hack他人的程式碼時,也會根據對方使用的設計模式採取一些額外的策略。例如,如果發現對方用的是正則表示式,我則會直接通過邏輯找正則的漏洞,然後定點hack。
3 改進方面和心得體會
改進方面
在與三角函式化簡相關的部分的複雜度較高,程式碼也略顯臃腫,沒能很好反應面向物件的思想。關於在迭代開發時,如何做到不漏細節也是一個值得思考的問題。本單元作業的所有bug基本都是由於細節的處理上的疏忽,而非整體架構設計的問題。或許在每次迭代開發結束後,靜下心來認真讀地從頭到尾讀一遍程式碼、整理一下不同類和方法之間的邏輯關係可以減少這種細節問題的發生。
心得體會
在面對一個複雜的工程問題時,應當花足夠的時間思考一個好的架構。好的架構應當完全反應問題的原貌,這個架構不能僅僅滿足於解決當前的問題需求,還應當考慮到後續的增量開發與迭代需求。在三次作業中,我基本沒有重構,第一次作業主要實現了遞迴下降解析和表示式樹的結構;第二次作業主要擴充了自定義函式和求和函式、實現了合併同類項;在第三次作業時,第二次作業的程式碼已經基本上完全符合第三次作業的需求了。
同時,本單元的作業也加深了我對OOP的理解:
-
封裝:封裝是指外部隱藏實現細節,僅提供必要的介面用於資料互動,實現“高內聚、低耦合”的狀態。一段需要重複使用的程式碼可以封裝成一個方法,一組在語義上關聯緊密的資料可以與管理這些資料的方法一起共同封裝成一個類。例如,我將在不同資料類中可能會重複用到的方法抽象成了一個公共的方法類;在每個方法的設計中,也儘量保證每個方法在邏輯上具有一個明確的含義,比較複雜的方法一般也會拆分成幾個子方法的組合。
-
繼承與多型:通過重寫方法,使得同一個方法在不同層級的類中有不同的表現;通過歸一化管理,由上層的抽象來實現無差別的引用和訪問。例如,我重寫了
equal hashCode toString clone
等方法,定義了Factor
介面,並在每個子類中重寫了介面中定義的方法。
另外,本單元作業使我初步瞭解了形式文法分析和遞迴下降法;通過理解形式化表述、遞迴下降解析和遞迴下降生成隨機資料,我對形式文法也產生了一定的興趣。