「BUAA-OO-Unit-1-HW4」第一單元總結
「BUAA-OO-Unit-1-HW4」第一單元總結
目錄Part 0 前言
OO第一單元作業主題是表示式化簡,具體為通過對錶達式結構進行建模,完成單變數多項式的括號展開,體會層次化設計和麵向物件的思想。如今,第一單元已經告一段落,在這裡再次對自己的學習內容和成果加以總結。
Part 1 第一次作業
第一次作業為對包含冪函式與常數的表示式進行化簡,涉及相對簡單的巢狀,UML類圖如下所示。
架構
資料儲存上,依照形式化表達設計為Expr->Term->Factor
。Parser
和Lexer
作為工具類,分別承擔句法分析和詞法分析的功能。
值得注意的是,本次架構中對於Factor
的設計較為不完善,事實上沒有區分Num
和Power
,而是將其統一為Unit
類,儲存一個形如\(a \times x ^ b\)的因子。
在讀取和儲存過程中,Parser
和Lexer
合作,構建了一棵多叉表示式樹。
Factor
介面為因子類的公共介面,所有的因子都實現了這個介面;Extendable
Term
都實現了這個介面。
下圖展示了這種讀取和儲存的過程的一個例子:
獲得答案的計算過程是和讀取解析相解耦合的。在呼叫頂層Expr
物件的toString()
方法後,逐層向下呼叫expend()
方法,得到展開式。這其中,得益於Unit
類的統一性,每一層向上返回的都是一個Unit
物件的集合,達到了形式上和介面的統一。
複雜度分析
本次作業部分方法複雜度如下圖,其餘方法複雜度較低,未在圖中體現。
actor()方法複雜度較高,分析原因知,設計該方法時對於常數和冪函式的解析都全部列在其中,沒有分別抽象為分別的方法;
Expr中的
case2方法和
caseMore方法複雜度較高,分析原因知,這兩個方法負責在輸出時處理較為具體的如
體現為
x*x等化簡情形,因此較為瑣碎,複雜度較高;
Expr中的
expend()`方法複雜度較高,原因是沒有將加法和合並同類項單獨抽象出來,而是一股腦寫在其中,導致複雜度較高。
測試
本次作業在中測和強測中沒有出現bug,在互測中出現bug。
其中,互測的bug在於程式碼中化簡\(-x\)打頭時忘記輸出。
在對room內其他同學互測時,發現兩個bug:一個是沒有處理\(0\)作為指數的情況;另一個是沒有處理表達式因子前有負號的情況。
同時,我在此次作業中設計並實現了一個自動化評測機,這對檢驗程式的正確性有不小的幫助,將在Part 4 自動化評測部分介紹。
總結
在設計本次作業之初,我設計過很多架構,在設計階段就推倒重來若干次。儘管在Pre中的冒險者遊戲中已經初步領會了面向物件的思想,但是面對較為抽象的表示式解析仍然顯得無從下手。
儘管如此,得益於training
部分提供的Parser
和Lexer
思路以及和助教與同學們的幫助,我最終得以確定這個較為面向物件的設計。但是,這個架構依然存在相當的不足,在後面的迭代開發介紹中將會著重介紹。
Part 2 第二次作業
架構
在本次作業中,我新增了SeflDefineFunc
類和Sum
類處理待解析表示式中的這兩類函式呼叫;新增了Func
類處理自定義函式的定義式;新增了Num
和Power
類將這兩類因子抽離出來;新增了Add
和Mul
方法處理加法和乘法;新增了Sin
和Cos
類處理三角函式;修改了Unit
類,其現在儲存形如\(a \times x^b \times \prod_{i=1}^{n}(sin(expr_i))\prod_{j=1}^{n}(cos(expr_j))\)的基元。
整體架構上,依然遵循Expr->Term-Factor
的結構,其中,Num,Power,SelfDefineFunc,Sum,Sin,Cos
都實現了Factor
介面。
在sum
和自定義函式的代入過程中,沒有采取在原字串暴力替換的方法,而是採用瞭解析出變數式再替換的辦法,更好符合了語義。
UML類圖如下所示,可以點開大圖檢視細節。
優點
- 簡化和統一了介面與計算,
Add
和Mul
始終只需要處理兩個Unit
集合(\(a \times x^b \times \prod_{i=1}^{n}(sin(expr_i))\prod_{j=1}^{n}(cos(expr_j))\))的加法和乘法即可。 - 資料儲存和運算相解耦合。具體來說,在解析原字串時,按照其結構儲存為一棵多叉樹;在運算過程中,從最頂層依次遞歸向下呼叫,獲得
Unit
集合,結構清晰,同時不更改原有儲存資料。
缺點
- 可擴充套件性不佳。其中的
Unit
類一旦遇到新的因子,就需要重構,對Add
和Mul
類也是同理。 - 涉及深淺拷貝問題,較為複雜和瑣碎。
-
Sin
和Cos
類可以設計為Tri
類的兩個子類,這樣更加符合其特點。
測試
-
本次作業在強測中出現了較大問題,具體為,設計解析三角函式方法時,忽略了形式化表達中指數可以帶有
+
與前導0的情況。仔細分析,原因應該是我沒有單獨將解析指數抽離出來,而是在每一個可能出現指數的地方都重寫了該操作。 -
對於替換變數後的
sum
函式和自定義函式,出現了可能不滿足Parser
和Lexer
要求的字串形式,即,沒有進行預處理,這是對細節處理的疏忽。
複雜度分析
圈複雜度較高的方法如上圖所示。其中,Parser
中caseSelfDefineFuncF/G/H
三個方法分別解析f,g,h自定義函式,其複雜度高的原因在於其中較多的進行了類似特判的操作,如在獲取引數的時候是否是一個引數以及是否引數讀取完畢。覆盤來看,這更應該分別抽離出來方法執行相應操作。
Expr
中的caseCos
和caseSin
方法複雜度高的原因在於,在輸出字串得時候,將化簡和獲取耦合在一起。舉例而言,對於sin(x)
,在該方法中特殊處理,使之不輸出sin((1*x))
。覆盤來看,更應該對於這種特殊情況再抽象出方法單獨處理,而不是耦合在其中。
Parser
中的caseSin()
和caseCos()
複雜度較高,原因在於,對於其因子單獨寫了讀取方法,而沒有利用已有的讀取整數和讀取冪指數的方法。這更應該抽離出來,在每個需要讀取指數的地方直接呼叫即可。
總結
這次作業在設計階段耗時過多,直到星期四才開始寫程式碼,擠壓了後面測試和優化的時間。覆盤看來,儘管設計和架構很重要,但是程式碼也很重要,應當兩條腿走路,不能顧此失彼。
經過和同學與助教討論,一種較為泛用的工程方法是,大致確定所需要的類和功能,然後在程式碼中逐步完善乃至小範圍重構。事實上,程式碼細節有相當多不夠漂亮乃至不得不很醜陋的地方,這是在設計階段難以考慮全面的,因此,應該在思路大體確定的情況下就快速進行程式碼開發,為後續的測試等留有餘地,這是本次作業最大的感悟。
Part 3 第三次作業
架構
本次作業進行了部分重構,具體為:擴充套件Sin
和Cos
類使之支援表示式因子的情況;新增RemoveWhite
類,將對錶達式的空白符即連續加減符號進行預處理的功能抽離出來;利用序列化與反序列化進行深拷貝。
優點
- 架構依舊較為清晰,遞迴層次分明,對資料儲存和資料處理解耦合。
- 體現了面向物件設計的特點。
缺點
- 可擴充套件性不強,如果還要新增如
tan
類等,需要對原有程式碼結構進行修改而非新增,並對Unit
類進行重構,對Add
和Sum
類進行重構等。
複雜度分析
圈複雜度較高的方法如上圖是,具體分析而言,本次作業中由於重寫equals()
方法,使得各層級的equals()
方法內容較多,複雜度較高;Add
和Mul
中則由於計算與合併同類項沒有解耦合,計算之後將結果插入時直接進行合併,使得這部分複雜度較高。
測試
本次作業在中測和互測中沒有出現問題,在強測中一個點判錯。
分析而言,是在合併同類項階段,對兩個Unit
集合是否相等的判斷出錯。出錯的方法為:將一個Unit
集合對映到另一個Unit
集合,如果每個元素都可以對映過去,則判等。事實上,這樣做的巨大缺陷在於,如果兩者為真子集關係,則會出現誤合併。針對該bug,修復辦法為判斷雙射才進行合併。
互測中,發現了sum
函式中上下限有的同學設定為int
的bug以及sum
中變數式為sin((-i))
的bug。這說明在細節處理方面仍然有很多需要注意地方,同時,測試只能找到bug,不能證明沒有bug,需要通過自動測試、覆蓋測試、針對測試等多種手段提高程式沒有bug的可能性。
效能
本次作業效能不佳,具體原因為對於sin
和cos
,沒有將可以去括號化簡的部分進行化簡,如:sin((x))
可以被化簡為sin(x)
。
總結
本次作業中,對於合併同類項的判等寫得瑣碎而不漂亮,而且沒有辦法證明完全沒有問題,最終出現了失誤。對於Unit
的設計思想,其對新需求的支援過弱,需要重新修改其內部大量細節,不夠漂亮。
效能方面,為了求穩妥沒有處理哪怕最簡單的拆括號,歸根結底是對測試沒有信心,自己在本地測試也不夠充分,這在以後應當加強。
Part 4 自動化測試
在本單元作業中,實現了一個自動化測試工具,其具體做法已經在討論區分享過,這裡附上鍊接「BUAA OO Unit 1 HW1」面向測試小白的簡易評測機
體會
測試機構建主要需要python
語言知識以及java
編譯知識,實現一個支援隨機資料的評測機在自測是很有用的,可以大規模覆蓋評測,這有很大幫助。
儘管測試機可以構造大量複雜度不同、情況不同的測試資料,但是對於特定資料可能不一定會恰巧出現,如有些同學設計了\(sin(x)^2+cos(x)^2=1\)的優化,這需要手動構造資料驗證,或在測試機中加入特殊資料池。
研討課分享時,我和大家分享了這個較為簡陋和樸素的評測機結構,也向其他班級分享的同學學習了很多新的思路,如姜雨竺同學分享的開閉性原則等,這開拓了我的視野和思路。
Part 5 單元總結
本單元作業中,我更加深刻體會了面向物件的設計思想。特別地,在寒假Per2作業中作為引入的冒險者遊戲的例子非常形象,很容易利用其理解面向物件以及Java語言的諸多特點,但是本單元作業相比較而言較為抽象的需求也可以很好地利用面向物件的思想,這是很大的收穫。
同時,在本單元作業中,我第一次親手實現了一個簡易評測機,並更加深入地體會測試在工程開發中的重要性。真實需求中可能沒有足夠充分和全面的資料供使用,需要自己構建資料乃至評測機以儘可能全面地檢查程式。
最後,在本單元作業中,老師、助教和同學們都給予了我相當大的幫助,數千行程式碼的書寫也進一步提升了我的程式碼能力,並將我從面向過程的舒適區中拽了出來。
Part 6 展望
本單元作業面對了諸多挑戰,也有諸多收穫,在以後的課程學習中,我希望在以下幾個方面可以更多加註意和學習:
- 設計、程式碼和測試並重。在第二次作業中,因為設計擠佔了過多時間導致了後面兩項工作時間倉促,工程產生了巨大問題。因此,以後應當設計出大概後便開始著手程式碼書寫,並儘可能構造測試資料進行測試。
- 更多注重設計模式。在本單元作業中,雖然知道有諸如工廠模式等優越的設計模式,但沒有認真學習和實現,而以後應當更多瞭解並使用這些經過實踐檢驗的方法。
- 不拖延ddl。OO作業具有越早開始越從容的特點,儘可能早地討論作業內容,設計大致思路會有很大幫助。
- 更多向大佬們學習。在本單元作業中,雖然有諸多助教和同學們的幫助,但是更多的我可能還是在單打獨鬥,這並不順利。因此,以後要更多和助教與同學們討論分享,頭腦風暴出好的設計和思路。