北航2022面向物件第一單元:表示式解析和化簡
北航2022面向物件第一單元:表示式解析和化簡
1. 發現的典型問題
1.1 物件深拷貝
在使用物件時,應該尤其注意物件的屬性是否在各種操作下都保持不變。特別是那些管理其他物件的物件。如果兩個容器類儲存了相同的物件引用,其中一個修改時,會把另一個容器中的物件一起修改,從而導致不可知的後果。
就這三次做的作業而言,時間上的要求不高,因此可以把所有管理資料的類全部設定成不可變的。或者獲得一個新的物件,必須完全地拷貝原來物件的全部資訊,包括容器內部的物件必須是地址不同的。
1.2 字串提取和解析
比較好的字串解析方法是遞迴下降,這樣就不用在一個方法內處理所有的問題,一個方法只用處理一種運算即可,邏輯比較簡單和清楚。但是這樣就必須知道目前頂層是哪一種運算。
可以使用棧來判斷頂層的運算。比如要判斷該層是否是加法,可以遍歷字串,如果遇到左括號就壓棧,遇到右括號就彈棧,只有遇到加號,而且此時棧為空時,才表示這個加號在頂層。
同樣的方法可以用來分割函式的引數。函式引數是用逗號分割的,而且引數可以是其他函式的呼叫。我聽到一種做法處理函式引數,就是把每兩個逗號作為分隔符,每組分別判斷是否是分隔符。這樣時間開銷相當大,而且分開的字串形式複雜,不好判斷。可以使用棧的方法解析函式引數字串。如果遇到逗號,而且此時棧為空,就說明這個逗號一定是頂層的,可以作為分隔符。
1.3 加減號和正負號的判斷
在第一次作業時,很多同學遇到了問題,不知道怎麼區分加減號和正負號。在第一次作業時,我用到的是無腦判斷。如果遇到一個 '+' 號,而且此時棧為空,就直接遞迴解析符號左右的字串。如果左右都是合法的字串,就可以認為這是個加號。
這種做法應該是沒有邏輯問題的,根據遞迴分析,錯誤的字串會在底層被識別,把錯誤資訊逐層上傳回來。但是在第二次作業中,因為這種方法我被hack了兩次,然而不是報邏輯錯誤,而是超時。這是因為輸入的字串非常長,每個項都有一個加號和一個正號。這樣我處理每個項,都必須遞迴分析整個字串。這種方法的時間複雜度是指數函式,字串稍微長一點耗時就非常長了。因此我被迫做了特判,就是字串的結尾不能是 '+' ,'-','*' 號,否則直接返回字串非法。因為輸入的字串一定是合法的,每次遞迴下降都是有效操作,這樣就保證了每次操作的時間開銷都是必要的。
2. 分析自己的程式
2.1 數量度量
指標:
指標名 | 作用物件 | 含義 | 方向 |
---|---|---|---|
LOC | 類/方法 | 程式碼行數 | 越小越好 |
LCOM | 類 | 類中內聚度的缺乏,值越小說明內聚度好,符合高內聚要求 | 越小越好 |
FANIN | 類 | 扇入表示呼叫該模組的上級模組的個數,值越大表示覆用性好。 | 越大越好 |
CC | 方法 | 圈複雜度,值越大說明程式程式碼質量低,且難以測試和維護 | 越小越好 |
第一次作業
類:
類名 | 簡介 | LOC | LCOM | FANIN |
---|---|---|---|---|
ExpFactory | 用來構造表示式 | 154 | 0.625 | 0 |
Expression | 表示式 | 102 | 0 | 3 |
Term | 表示式中的項 | 62 | 0.286 | 1 |
Main | 主類 | 10 | -1 | 0 |
方法(只記錄主要方法):
類名 | 方法名 | 簡介 | LOC | CC |
---|---|---|---|---|
ExpFactory | createExp | 建立表示式 | 45 | 11 |
createExpByTerm | 建立項 | 46 | 12 | |
createExpByFactor | 建立因子 | 32 | 7 | |
simplifyStr | 簡化字串 | 3 | 1 | |
Expression | toSimpleStr | 簡化字串 | 28 | 7 |
power | 乘方 | 18 | 4 | |
multiExp | 乘表示式 | 18 | 4 | |
subExp | 減表示式 | 4 | 1 | |
negate | 取負號 | 5 | 2 | |
addExp | 加表示式 | 14 | 3 | |
addTerm | 新增一個項 | 9 | 2 | |
Term | toSimpleStr | 簡化字串 | 40 | 13 |
第二、三次作業
因為第二、三次作業的設計思路基本一樣,所以用第三次作業來說明
類:
類名 | 簡介 | LOC | LCOM | FANIN |
---|---|---|---|---|
Main | 主類 | 47 | -1 | 0 |
AddSubFunc | 加減函式 | 23 | 0 | 3 |
Func | 函式基類 | 117 | 0 | 10 |
FuncFactory | 用來建構函式 | 227 | 0 | 2 |
FuncLib | 自定義函式庫 | 105 | 0 | 2 |
MulFunc | 乘法函式 | 18 | -1 | 2 |
NumFunc | 常元函式 | 19 | 0 | 4 |
OpFunc | 正負函式 | 19 | 0 | 2 |
PowerFunc | 指數函式 | 23 | 0 | 2 |
SumFunc | 求和函式 | 64 | 0.5 | 2 |
TriFunc | 三角函式 | 23 | 0 | 3 |
VarFunc | 變元函式 | 23 | 0 | 6 |
Output | 用來輸出的類 | 150 | 0 | 3 |
StrUtil | 字串工具類 | 37 | -1 | 3 |
Term | 輸出類的組成單元 | 157 | 0 | 2 |
方法(只記錄主要方法):
類名 | 方法名 | 簡介 | LOC | CC |
---|---|---|---|---|
Func | toOutput | 得到輸出的物件 | 52 | 12 |
replaceFormal | 將所有變元替換為新函式 | 12 | 4 | |
replace | 將該函式節點替換為新函式 | 10 | 2 | |
duplicate | 複製 | 23 | 4 | |
FuncFactory | createFunc | 建立函式 | 47 | 12 |
createSumFunc | 建立求和函式 | 33 | 6 | |
createTriFunc | 建立三角函式 | 15 | 3 | |
createOpFunc | 建立符號函式 | 13 | 3 | |
createAddSubFunc | 建立加減法函式 | 29 | 7 | |
createPowerFunc | 建立指數函式 | 31 | 7 | |
createMulFUnc | 建立乘法函式 | 33 | 8 | |
createVarFunc | 建立變元 | 10 | 2 | |
createNumFunc | 建立常元 | 10 | 2 | |
FuncLib | addFunction | 新增自定義函式形式 | 5 | 1 |
createCustomFunc | 建立自定義函式呼叫 | 35 | 7 | |
getRealParamStrings | 解析實參字串 | 45 | 12 | |
Output | duplicate | 複製 | 7 | 2 |
getString | 獲得化簡字串 | 26 | 7 | |
power | 乘方 | 15 | 3 | |
mul | 乘法 | 22 | 6 | |
sub | 減法 | 5 | 1 | |
add | 加法 | 19 | 5 | |
negate | 取負 | 8 | 2 | |
addTri | 新增三角函式 | 10 | 3 | |
addNum | 新增常數 | 4 | 1 | |
addVar | 新增變數 | 4 | 1 | |
isNumFactor | 是否是常量因子 | 7 | 2 | |
isVarFactor | 是否是變數因子 | 17 | 5 | |
Term | duplicate | 複製 | 10 | 3 |
getString | 獲得化簡字串 | 53 | 16 | |
mul | 乘法 | 30 | 7 | |
isNumber | 是否是常數 | 9 | 3 | |
isPower | 是否只含有變數指數 | 9 | 3 | |
isSimilar | 是否是同類項 | 6 | 2 | |
StrUtil | noBlank | 去除空白 | 6 | 2 |
noBracket | 去除兩端對應的括號 | 29 | 8 |
2.2 發現的bug位置(第二、三次作業)
-
筆誤
在 FuncFactory.createPowerFunc() 方法中,使用到了很大的迴圈,迴圈中的一個判斷條件寫錯。此處程式碼31行,是同類方法行數第二長的。 -
超時
在 FuncFactory.createFunc() 中沒有特判,導致遞迴呼叫超時。該方法47行,是該類方法中最長的,該方法的CC為12,也是該類方法中最大的。這導致這個函式不好除錯,而且一旦出錯會影響到很多地方。 -
重寫錯誤
在 MulFunc.duplicate() 方法中,重寫Func.duplicate(),忘記遞迴呼叫乘法兩邊的複製方法,導致該方法複製不完全,沒有達到深拷貝的效果。
2.3 結構圖
-
第一次作業
-
第二、三次作業
3. 測試策略
因為時間所限,只有第一次作業做了比較完全的測試。因為整個結構是比較有規律的,二、三次作業根據1.1的易錯點進行測試,即可基本保證不出問題。完全測試基本上是根據形式化表述,從底向上,逐層測試。比如先測試各種因子對不對,然後構造項進行測試,最後構造表示式進行測試。
因為時間所限,我沒有具體針對程式碼進行互測,只是隨意交了一些資料進行測試,當然也沒有查到別人的bug。我認為,最大的問題可能就是物件深拷貝,其次是函式多次呼叫和迴圈呼叫的問題。如果這兩個點都是正確的,我相信其他地方也不會有很大問題。
4. 設計體驗
4.1 架構迭代
因為第一次作業只有多項式,所以我採用了很簡單的方法解析,專門針對多項式。然後根據助教的提醒,後期的作業需要更大範圍的抽象,所以只能重構了。第二次作業因為時間不夠,加上整個架構全部要重寫,所以作業效果並不好。但是架構設計好了以後,為第三次作業帶來了很大的方便。第三次作業在第二次的基礎上,修改行數估計只有二三十行,修改時間估計一兩個小時。而且在強測和互測也沒有什麼錯誤。我認為只要把易錯點都檢查過了,就不會有什麼問題。
4.2 心得體會
第一單元是進入OO課程的第一次訓練。通過三次作業,我逐步地提高設計的抽象程度,以應對增加的需求。我原先認為,第一次作業和第二、三次的銜接不夠,針對第一次作業的設計結構到後面幾乎都要重構,否則會變得非常複雜。但是現在我認為這也不是什麼問題。因為之後確實有可能會提出各種需求,而這是在之前的作業中不能預計的。所有的遠見都是有限的,不可能考慮到所有的情況。我個人認為,程式首先要滿足當前的需求和效能,其次才是預留未來的需求。我覺得要抓住主要矛盾和主要方面,優先滿足當前需求,該重構時就重構,我聽說unix還是linux這麼龐大的系統都重寫了幾次。
關於作業量和難度,我因為之前看過一些Java的教程,所以感覺難度不是很大,只是作業量有點多,需要平衡這個課和其他的課的時間分配。但是我也聽說有的同學感覺有些難度,可能有一次作業做得不是特別好。如果我之前沒有看過相關知識,可能也會感覺比較吃力。我覺得這實在是沒辦法的事情,想要用好一門語言,至少要知道語法和常用用法。在pre中有比較好的練習,因為pre不是強制的任務,可能有些同學就沒做了。