2019面向對象課設第一單元總結
一、三次作業總結
1. 第一次作業
1.1 需求分析
第一次作業的需求是完成簡單多項式導函數的求解,表達式中每一項均為簡單的常數乘以冪函數形式,優化目標為最短輸出。為了滿足優化目標,我們需要將含有相同指數的項進行合並。
1.2 實現方案
根據需求,我們很容易就能想到利用HashMap
構建常數和冪指數的對應關系(再加上這是第一次作業,本以為只是讓我們借此熟悉一下Java語法,於是並沒有考慮程序可擴展性),於是僅建立了Polynomial
多項式類和用作主函數的TestDerivative
類(本人的風格一直是建立一個單獨的main函數類,在其中保持盡可能小的代碼規模,盡量10行以內解決問題),並利用Polynomial
類中的屬性HashMap<BigInteger, BigDecimal> terms
(在第一次作業時我錯誤地認為還可以識別浮點數)進行表達式信息的存儲。對於錯誤處理,由於錯誤輸入的判斷可能在程序中的多處出現,於是我設立了用於檢測錯誤的IllegalInputException
類作為程序中使用的輸入異常類,在發現輸入出錯時將其拋出,並通過調用逐層向上傳遞至main函數進行統一的捕捉處理。程序UML圖如下:
求導程序具體實現中的難點在於三處:輸入串處理、求導以及表達式輸出。
在本程序中對於輸入串的處理僅僅利用了Polynomial
類的parse()
函數,首先利用正則表達式進行非法字符/非法空格的判斷,接着刪去所有空格進行整體表達式的合法性判斷,再根據正負號進行切分,對切分出的每個項根據*號和^號分離出系數和指數填充HashMap
。在填充時,對於HashMap
中已有的項不能進行替換而是需要進行系數相加,於是還需要多一步判斷。在求導時,對於HashMap
的每一項遍歷計算產生新項是非常簡單的,最終返回一個新的Polynomial
。輸出時,由於當時並不知道toString()
方法的重寫,於是自己寫了一個print()
函數。
1.3 結構分析
此次作業中,解決關鍵問題的僅有Polynomial
類中的parse()
(一個函數解決輸入處理)、calculate()
(一個函數解決求導)、print()
(一個函數解決輸出)。
本次作業的度量如下:
指標 | 項目平均值 | 項目總值 | 特殊值 |
---|---|---|---|
Lines of Code(總行數) | 58 | 176 | |
Lines of Code per Method(方法行數) | 17.625 | Polynomial.parse() - 60 |
|
Essential Cyclomatic Complexity(本質復雜度) | 1.75 | Polynomial.parse() - 5 |
|
Design Complexity(設計復雜度) | 3.75 | Polynomial.parse() - 13 |
|
Cyclomatic Complexity(循環復雜度) | 5.25 | Polynomial.parse() - 18,Polynomial.print() - 15 |
|
Average Operation Complexity(平均操作復雜度) | 4.25 | Polynomial - 5.33 |
|
Weighted Method Complexity(加權方法復雜度) | 11.33 | 34 | Polynomial - 32 |
Cyclic Dependencies(循環依賴) | 0 | 0 | |
Depth of Inheritance Tree(繼承樹深度) | 0 | 0 |
本次作業不愧是我寫的第一個正經OO程序(先前編譯課設雖然用C++雖然也按功能分了不同類,有一點OO的苗頭,但奈何所有字段都是public,住手這不是OO),一分析就發現充滿了各種各樣的問題,其中還有兩個UA類都沒寫在表格里(一個是主類,一個是異常類,這個任務下異常類難道除了extends Exception
之后調用super()
方法以外還需要寫別的么)。在這里面,Polynomial.parse()
的問題極大,可以看出其驚人的60行頂格代碼行數帶來的是極大的復雜度,甚至連設計復雜度都很大,這意味着對於字符串的處理不僅應該分成若干個更小的函數,將判斷字符串合法和分析字符串兩項功能分開,同時自己在處理字符串時運用的算法也是開銷極大的,有不小的優化空間。實際上此次在分析字符串時,我采用的不是正則表達式匹配,而是采用了字符遍歷的方式,這種方式需要使用的的String
類函數調用使得設計復雜度大幅度提升。對於Polynomial.print()
函數,其復雜的優化邏輯同樣讓循環復雜度相當大,其中充斥着對HashMap
的各種操作。反思一下,雖然在此次任務中利用StringBuilder
將系數、x和指數進行字符串拼接是一種較為直接的想法,但是其復雜度卻很高。由於本次只有一種對象,循環依賴和繼承樹深度均為0。
1.4 測試思路
此次作業相對簡單,測試的思路也並不復雜,只需要按照指導書對每一種不同的省略輸入進行測試即可。由於代碼邏輯較為簡單,Debug沒有給我留下什么深刻印象,線上測試也沒錯,因此也省略下面的Bug分析部分。
2. 第二次作業
2.1 需求分析
第二次作業需要完成簡單冪函數和簡單正余弦函數求導。相比於第一次作業,增加了因子類別sin(x)和cos(x),且因子之間可以相乘了。優化目標依然是最短輸出,優化方案有二:合並同類項,利用cos(x)+sin(x)=1進行展開或合並。
2.2 實現方案
在第二次作業的實現中,受第一次作業“對應關系”思路的影響,我依然在尋求優化過程中存在的對應關系。對於項的合並,此次任務中沒有了系數和指數的對應關系,然而,由於合並同類項的過程中不存在括號,能夠合並的項僅有除系數外完全相同的項,因此在多項式中存儲的應為項與系數的對應。對於因子的合並,需要對同類型因子進行合並,所以此處的對應關系為因子類型與指數的對應。因此,我建立了描述因子類型的枚舉類FactorType
用於存儲四種類型因子的正則表達式並實現了查看匹配何種類型因子的方法(這么看來有種工廠模式的影子?)。對於因子類Factor
,存儲因子類別、系數和指數,為求導時產生帶系數的項做准備;對於項類Term
,存儲因子類別與因子對應的HashMap
;對於多項式類Polynomial
, 建立項類別與項對應的HashMap
。為此,需要在FactorType
,Term
兩個類中重寫equals()
和hashCode()
方法以識別同類型因子類型或項。程序UML圖如下(復雜了不少呢):
本次作業除了上次繼承的三個主要問題外,還新增了優化問題。
對於輸入處理,本次去掉了無用的間接處理方法,而是直接在構造器中進行分析,實現了高內聚。此外,由於表達式更加復雜,我轉而使用表達式樹的逐層正則判斷方式:在Polynomial
中,將輸入串根據符合條件的+-號位置將表達式切分成一個個項字符串,將每個字符串交由Term
處理;在Term
中,將傳入的輸入串根據符合條件的*號位置將項切分成一個個因子字符串,將每個字符串交由Factor
處理;在Factor
中,調用工廠函數FactorType.parse()
完成因子類型判斷+系數/指數分析。由於此處的FactorType
類為枚舉類型,使得4種因子類型遍歷更為方便,同時在后續作業因子更為復雜時可以直接將該類轉為接口,以便后續作業添加更多因子類型,極大地增加了可擴展性。
對於求導處理,在不同的類中采用不同的求導策略:對於因子,在Factor
類中分因子類型采取不同的求導策略,實現鏈式法則;在Term
類中對因子遍歷,利用雙重循環每次保證只有外層循環遍歷到的因子求導,其余因子保留,實現乘法法則;在Polynomial
類中對項遍歷,將結果相加,實現加法法則。
對於輸出,由於表達式樹的每一層均重寫了屬於該類的toString()
方法,使得底層只需考慮最簡單的單因子轉換,而類/表達式僅需將底層傳上來的字符串進行簡單符號連接即可,這種分對象進行處理的方式免去了多種情況討論的復雜邏輯,十分方便且耦合較為簡單。
對於優化,我依然對於可合並的項和因子重寫equals()
和hashCode()
方法判斷是否可合並,從而最大限度地合並同類項/同類因子。對於sin(x)和cos(x)的合並問題,該問題是一個僅能尋求近似解的問題:由於可能涉及到項的拆分,所以該任務不能僅由簡單的貪心完成;同時,拆分時能采取的公式並不止sin(x)+cos(x)=1一個,諸如平方和/立方和公式都可以使用,這大大增加了優化成本;論壇內的DFS+貪心策略雖然可以實現,但風險過大,由於性能分僅占20%,正確性依然是首先需要保證的,所以我僅僅實現了最簡單的sin(x)2+cos(x)2=1的合並。實現方法是對於每一次出現sin(x)2或是cos(x)2,都計算其對偶項(也就是將cos(x)替換為sin(x),反之亦然)的項,並在表達式中進行查找。雖然低效,卻也能實現最基本的功能,不過如此簡單的合並在實際操作中很難遇到可以進行優化的情形。
2.3 結構分析
本次作業將三個主要任務下放至三個層次的類中分別處理該層的對應特征。本次作業的度量如下:
指標 | 項目平均值 | 項目總值 | 特殊值 |
---|---|---|---|
Lines of Code(總行數) | 104 | 627 | |
Lines of Code per Method(方法行數) | 11.731 | Factor.calcFactorDiff() - 51 |
|
Essential Cyclomatic Complexity(本質復雜度) | 1.67 | Factor.toString() - 9, FactorType.parse() - 5 |
|
Design Complexity(設計復雜度) | 3.09 | Polynomial.toString() - 13, Factor.calcFactorDiff() - 11, Factor.toString() - 9, Term.getDualTerm() - 9 |
|
Cyclomatic Complexity(循環復雜度) | 5.25 | Polynomial.toString() - 15, Factor.calcFactorDiff() - 11 |
|
Average Operation Complexity(平均操作復雜度) | 2.88 | Polynomial - 4.44 |
|
Weighted Method Complexity(加權方法復雜度) | 20.67 | 124 | Polynomial - 40, Term - 40, Factor - 36 |
Cyclic Dependencies(循環依賴) | 0 | 0 | |
Depth of Inheritance Tree(繼承樹深度) | 0 | 0 |
本次作業在將不同種對象在類中加以區分之后在各項指標的平均值上均出現了下降,這表明面向對象的思路的確可以通過將單一任務區分為不同類對象的分任務的方式降低程序復雜度,同時讓編程思路更加清晰。但是,此次OO的思路依然沒有貫穿始終,從因子求導方法Factor.calcFactorDiff()
的高復雜度就能看出:多種因子實際上應該分為子類繼承於Factor
類之下,而非將所有類型的因子都歸為一類並在方法中利用低效switch
語句進行區分。Polynomial.toString()
類依然繼承了第一次作業的寫法,復雜度依然居高不下。Term
類由於加入了很多優化判斷方法,故其WMC很高。
2.4 測試思路
本次測試需要生成大量表達式,而若是通過自己思考生成表達式則容易在構造樣例時采用與代碼同樣的思路,從而繞開自己潛在的錯誤。為此,我利用Python腳本,運用Xeger包生成符合要求的表達式,並調用Mathematica進行fuzzing測試,通過與自己的結果進行比較來探求程序中的bug。
Mathematica的優點在於其能夠快速生成表達式的導函數,並能夠直接通過表達式進行等價判斷。但是在引入三角函數后,其復雜的誘導公式使得較長的公式無法直接進行判斷,這也直接導致了測試中的表達式串都相對偏短。但是在測試中,長度帶來的限制是容易自己構造的,因此我在自動測試中簡單地跳過了無法判斷的式子。這在一定程度上增加了fuzzing失敗的風險,但是不失為當時的一種可行方案。最終發現的bug較少,最終測試中也並沒有問題,所以跳過bug部分。
2.5 總結反思
此次程序中有幾個設計不到位的地方:首先是在Term
中Expression
中實現的對應關系不對稱,這讓身為強迫症的我寫代碼總有一種不舒服的感覺…歸根到底,這種失敗設計的根源在於沒有意識到系數實際上是一種特殊的因子,這讓我在處理系數時相當棘手。在今后的設計中,應該保持觀察對象的敏銳性,進一步發現問題中每個元素的更高抽象層次。
其次,FactorType
類的設計初衷是為了增加程序的可擴展性,以便在之后需求增加時能夠快速反應。但是,可擴展性的弊端在於其弱化了程序的建模:在此次作業中總共有4種不同類型的因子,因此正如討論區所說,每一個項均可以看做由一個5維向量存儲的對象,這5個量分別代表了一個項的系數和四種因子的次數。由此,優化時可以不需要再通過Term
深入兩層獲得其某些因子的信息,而是可以直接通過其向量進行優化。在設計類時,除了考慮其實際意義與可擴展性外,同樣應該看到當下的限制對當下項目的特殊性:往往多加的限制是對當下項目簡化建模結構,方便優化的必要條件。在可擴展性和建模的簡化性上不應該像這次項目一樣一味地偏向一方(實際上第三次作業依然對本次設計做了小重構,真是失敗呢),而是需要做好兩邊的權衡。
3. 第三次作業
3.1 需求分析
第三次作業加入了表達式和sin/cos因子嵌套。嵌套使得可以通過括號進行更大范圍的同類項合並,同時表達式樹深度也可以大幅增加。
3.2 實現方案
在該任務中,已經很難看到之前作業那種很明顯的對應關系,因為因子和項已經可以嵌套,而且可以變得相當復雜。因此,我延續了第二次作業的表達式樹結構,並將系數歸於因子管理、系數作為一種特殊的因子歸於項管理。同時,在此次作業中我實現了工廠類Factor
,並在每一種類型的Factor
中放入了符合該類型因子特征的正則表達式REGEX
作為其屬性,並標記public static final
對Factor
類開放,從而讓工廠可以自動地通過逐一判斷返回恰當的因子類型。在存儲上,此次改用了HashSet
以方便進行表達式/因子合並優化。程序UML圖如下:
對於輸入處理,依然延續了先前的在Expression
中根據+-截斷,在Term
中根據*截斷,並最終給Factor
進行因子匹配的思路構建表達式樹。與先前不同的地方在於,此次由於因子內依然可以包含表達式,所以需要在每一次截斷時進行多一步匹配:必須使得截斷的符號在最外層括號之外。此處,在分析開始前,我首先利用Parser
類進行輸入串的初步分析,包括是否為空/非法字符探測/非法空格探測等等。在刪除掉所有空格后,首先遍歷輸入字符串,並利用棧(此處並不需要維護一個真正的Stack
,而是可以利用一個初值為0的數int parenStack
通過加1和減1操作模擬壓棧和彈棧)維護當前字符處的括號情況,當棧為空(即當前符號不在任何一對括號內)且當前字符為需要截斷的字符時,向其之前增加一個特殊字符作為標記,在遍歷完成后根據該特殊字符進行截斷進行下一層分析。對於表達式類ExprFactor
中表達式的提取,只需要利用String.substring(1,str.length()-1)
將外層的兩個括號去掉,傳給Expression
類進行匹配即可。在任何一步中出現正則表達式無法匹配的情況,都會拋出一個IllegalInputException
,由函數逐層穿給其調用函數,直至拋給main()
進行錯誤輸出。這樣的匹配模式不需要一個統一的輸入處理類,而是由各個類自己利用構造器根據輸入字符串構造屬於自己的對象,將任務進行了分派,更符合面向對象的思想。
對於求導處理,此次的實現更為清晰:所有可以求導的對象(包括Factor
及其子類,Term
和Expression
)均實現求導接口Derivable
,並在因子類中實現鏈式法則,在項類中實現乘法法則,在表達式類中實現加法法則。求導的結果最終會由表達式樹的低端依次向上傳遞,直至傳遞到最高層的Expression
中。
對於輸出處理,同樣分派給各個類去實現自己的toString()
函數。這樣可以將輸出的優化盡可能分散開,便於查找錯誤。
對於優化,此次由於優化分數很少,所以並沒有實現有關sin/cos的優化。然而,此次依然進行了合並同類項以及合並同類因子的操作:由於在表達式的每個層次中均采用HashSet
進行存儲,其目的是在每一個表達式中只存儲不能合並的項以及在每個項中存儲不能合並的因子,所以對equals()
和hashCode()
的重寫需要使得能合並的項相等。但是,對於每個因子而言,其括號內部的表達式/項必須完全一致才能保證可合並,因此,需要實現一個表示對象完全相同的函數fullEquals()
。這樣,通過兩個功能不同的equals()
和fullEquals()
即可輕松實現對HashSet
中可合並項的查找及合並。
3.3 結構分析
本次作業的度量如下:
指標 | 項目平均值 | 項目總值 | 特殊值 |
---|---|---|---|
Lines of Code(總行數) | 69 | 1027 | |
Lines of Code per Method(方法行數) | 7.6 | ||
Essential Cyclomatic Complexity(本質復雜度) | 1.71 | Term.equals() - 7, CosFactor.CosFactor() - 7, SinFactor.SinFactor() - 7, Factor.parseFactor() - 6, Parser.parenMatchingCheck() - 6 |
|
Design Complexity(設計復雜度) | 2.18 | Expression.Expression() - 12, Term.toString() - 11 |
|
Cyclomatic Complexity(循環復雜度) | 2.55 | Expression.Expression() - 16, Term.toString() - 12 |
|
Average Operation Complexity(平均操作復雜度) | 2.18 | Term - 3.93, Parser - 3.67, Expression - 3.50 |
|
Weighted Method Complexity(加權方法復雜度) | 17.64 | 194 | Term - 55, Expression - 35 |
Cyclic Dependencies(循環依賴) | 6 | ||
Depth of Inheritance Tree(繼承樹深度) | 1.3 | 3 |
本次作業在各項復雜度指標上相比於上一次作業又有了大幅下降,這表明對於各個類的功能分拆取得了很大成效。而Expression
構造器的復雜度較高可能是因為沿用了先前的處理方式吧(笑)。Term
類作為聯系表達式和因子的橋梁,其WMC值較高也是有一定道理的。在方法的本質復雜度上,所有經過了字符串遍歷用棧來維護括號對應的方法復雜度都較高,因為其中出現了一些較復雜的判斷,無法避免,而Expression
構造器中依然不僅存在括號棧的問題,還需要根據其前2位是否為符號進一步判斷調用Term
的子串起始位置,所以出現了復雜度較高的情況。實際上,可以將這兩部分分成兩個方法。
3.4 測試思路
本次測試我利用了加強的Python腳本進行測試。Mathematica雖然功能強大,但是與Python的交互需要通過subprocess
調用命令行完成,甚至必須經過讀寫文件的過程,系統開銷巨大;與此同時,Mathematica的表達式等價性判斷也局限了表達式復雜度。在此次fuzzing測試中,我同樣利用Xeger包生成表達式,轉而利用Anaconda原生的代數庫Sympy進行測試。
在測試中,字符串的生成是一個挑戰,由於在Java項目中利用單一正則表達式進行輸入匹配已經相當具有挑戰性,利用Xeger一次性生成所有可能表達式也很有難度。因此,我利用構造表達式樹的反向思維,首先利用Xeger生成一些帶臨時標志的表達式/項/因子模板,再進行循環,對上一輪增添的臨時標志用新生成的串進行替換。為了避免替換的串一直含有臨時標志導致循環無法終止,我設定了max_round
常數用於終止循環,當循環次數大於max_round
時只能向其中填充冪函數項,由於冪函數項不存在因子/表達式參數,所以可以保證其生成的內容中不含有臨時標志。對於每一次填充因子時,首先隨機生成一個0~4之間的數,表示此次生成因子的類型,而后根據對應的因子正則表達式生成該類型因子進行臨時標志替換。此次自動測試的表達式生成代碼如下(其中臨時標志的設置為:'@'表示應該填充項,'#'和'~'表示應該填充因子,'!'表示應該填充表達式):
expressionRegex = "^(@[+-]{1,2})*@$"
termRegex = "^(#\\*)*#$"
sinFactorRegex = "^sin\\(~\\)(\\^(\\+)?[1-9]\\d{0,1})?$"
cosFactorRegex = "^cos\\(~\\)(\\^(\\+)?[1-9]\\d{0,1})?$"
powerFactorRegex = "^x(\\^(\\+)?[1-9]\\d{0,1})?$"
constFactorRegex = "^[+-]?(([1-9]\\d{0,1})|0)$"
exprFactorRegex = "^\\(!\\)$"
x = Xeger(limit=2)
def generate():
max_round = 2
result: str = x.xeger(expressionRegex)
generate_round = 0
while True:
generate_round += 1
for termCount in range(result.count('@')):
result = result.replace('@', x.xeger(termRegex), 1)
for factorCount in range(result.count('#')):
factor_type = random.randint(0, 4)
if factor_type == 0:
if generate_round > max_round:
result = result.replace('#', x.xeger(powerFactorRegex), 1)
else:
result = result.replace('#', x.xeger(sinFactorRegex), 1)
elif factor_type == 1:
if generate_round > max_round:
result = result.replace('#', x.xeger(powerFactorRegex), 1)
else:
result = result.replace('#', x.xeger(cosFactorRegex), 1)
elif factor_type == 2:
result = result.replace('#', x.xeger(powerFactorRegex), 1)
elif factor_type == 3:
result = result.replace('#', x.xeger(constFactorRegex), 1)
elif factor_type == 4:
if generate_round > max_round:
result = result.replace('#', x.xeger(powerFactorRegex), 1)
else:
result = result.replace('#', x.xeger(exprFactorRegex), 1)
for innerCount in range(result.count('~')):
factor_type = random.randint(0, 4)
if factor_type == 0:
if generate_round > max_round:
result = result.replace('~', x.xeger(powerFactorRegex), 1)
else:
result = result.replace('~', x.xeger(sinFactorRegex), 1)
elif factor_type == 1:
if generate_round > max_round:
result = result.replace('~', x.xeger(powerFactorRegex), 1)
else:
result = result.replace('~', x.xeger(cosFactorRegex), 1)
elif factor_type == 2:
result = result.replace('~', x.xeger(powerFactorRegex), 1)
elif factor_type == 3:
result.replace('~', x.xeger(constFactorRegex), 1)
elif factor_type == 4:
if generate_round > max_round:
result = result.replace('~', x.xeger(powerFactorRegex), 1)
else:
result = result.replace('~', x.xeger(exprFactorRegex), 1)
for exprCount in range(result.count('!')):
result = result.replace('!', x.xeger(expressionRegex), 1)
if result.find('@') == -1 and result.find('#') == -1 and result.find('~') == -1 and result.find('!') == -1:
break
return result
在生成目標字符串之后,我將20個數分別帶入Sympy計算的導數以及Java程序計算的導數中,並比較二者差值與1e-6的大小來確定自己的程序是否出現錯誤。
利用該測試程序,我可以通過設置第8行的limit
和第12行的max_round
分別改變生成串的單個元素長度以及式子的嵌套深度。由於替換算法較為復雜,當兩個參數超過3時生成表達式會異常緩慢,不過在兩個參數分別為2、3和3、2時生成的表達式已經足夠長,嵌套深度也較大;此外,通過修改正則表達式,還可以進行系數為0等針對性測試。利用該自動測試,我發現了不少兩個equals()
使用相反的bug。除了equals()
和fullEquals()
用反的bug外,對於對象引用的操作也是一個易錯點,對此,最簡單的解決方案就是在需要傳對象應用的類實現Clonable
接口並重寫clone()
方法。但是,這一偷懶的操作不得不說成為了之后我犯錯的伏筆。
3.5 bug修復
在此次強測前,我利用自己的自動測試程序做了10000+次fuzzing測試,確保自己的程序正確性沒有問題。然而,在本次的強測中,我被判了4個點的CPU_TIME_LIMIT_EXCEED,這是我絕對沒有想到的。在這4個測試點中,均出現了表達式因子的大量括號嵌套,而在本地測試中,我僅通過5層括號嵌套確保了自己的正確性,卻沒有發現,當表達式因子的括號為7層時運行會出現0.5秒左右的卡頓,而括號大於10層時已經無法看到運行結果。在平時的測試中,我的自動測試程序曾生成過7層sin/cos嵌套的情況,可以瞬間輸出答案,而這是因為因子中的參數是另一個因子;而表達式因子中,參數作為一個表達式,對其進行拆解需要經過表達式→項→因子三層,當括號嵌套過多時則會出現內存不夠用的情況。對此,正確的修復應該是在每一次表達式因子ExprFactor
調用Expression.Expression()
構造器前,首先探尋最內層括號的位置,直接通過字符串操作將多余括號略去。
3.6 總結反思
此次作業真的是白 優 化 了,在准備時只考慮正確性卻未曾考慮過超時/超內存的我被教了很關鍵的一課。但是從好的方面想,這次慘痛的教訓讓我了解到,在將來思考代碼結構時,不能把內存看作是一種無限的資源,而是要盡可能在保證正確性的情況下減小內存占用,無論是否關乎正確性,在進行數據預處理時就將能節約內存/運行時間的優化率先做完,會讓自己在之后的編碼中少一些顧慮。此外,此次的內存爆炸很大程度上與我的自動測試程序給我的盲目自信相關:碰巧由於自動測試程序無法生成嵌套層數過多的表達式因子,這導致自己無法生成會導致內存錯誤的樣例,這說明正確性測試/邊界條件測試是不能替代壓力測試的,極限情況不僅包含數據邊界,更包含壓力邊界,前者決定了能否在任何時候輸出的是正確結果,后者決定了能否在任何時候輸出。
二、創造性模式應用
在本次作業中,雖然自己沒有意識到,但是第三次作業中的Factor
類實際上應用了工廠模式,通過實現parseFactor()
方法,我對於每一個傳入的項字符串都對各個種類因子分別進行了正則匹配以尋求正確的返回類型:
static Factor parseComplexFactor(String input)
throws IllegalInputException {
if (input.matches(SinFactor.REGEX)) {
return new SinFactor(input);
} else if (input.matches(CosFactor.REGEX)) {
return new CosFactor(input);
} else if (input.matches(PowerFactor.REGEX)) {
return new PowerFactor(input);
} else if (input.matches(ConstFactor.REGEX)) {
return new ConstFactor(input);
} else if (input.matches(ExprFactor.REGEX)) {
return new ExprFactor(input);
} else {
throw new IllegalInputException();
}
}
由於本次作業中,各類因子的添加需要在同一個位置進行判斷,它們又繼承自同一個類,所以在本次作業中非常適合應用工廠模式。在實際應用中,我使用的是一個簡單工廠,通過工廠類中提供的函數進行構造。但是,Factor
類作為所有因子的父類,不應該再多實現一個工廠功能,而是應該單獨建立工廠類進行操作;同時,這種朴素的實現方法等價於用switch-case對所有子類進行遍歷,當子類類型增加時還需要對工廠類進行修改,不符合開閉原則。一個符合開閉原則的實現方式是利用反射機制,還有一種是不利用反射機制的類注冊模式。這種模式在每一個子類中增加注冊函數對工廠類傳入該子類的注冊信息,在工廠類中維護一個HashMap
維護一個標簽和類/實例的映射從而實現在工廠類中的便捷遍歷,這種實現符合開閉原則,雖然稍顯復雜但可擴展性得以增強。
三、特典金曲《沒 BUG 人(TV Size)》
在總結的最后的最后,有感而發為大家獻唱一曲《沒 BUG 人》(霧,我自己還遠遠沒有達到這個層次),與大家共勉~
噔蹬蹬↗ 蹬蹬 蹬 蹬(鏡頭放大到bug上)
あれは誰だ 誰だ 誰だ 那是誰 是誰 是誰
あれはNO BUG NO BUG MAN NO BUG MAN 那是沒BUG 沒BUG人 沒BUG人
『沒人能說自己沒有BUG』の 名をうけて 背負着『沒人能說自己沒有BUG』的名義
すべてを捨てて たたかう男 舍棄了一切(指課余時間)去戰斗的男人(?)
NO BUG MANアローは Object Oriented 沒BUG人之箭是面向對象
NO BUG MANイアーは Programming Style 沒BUG人之耳是風格規范
NO BUG MANウイングは JUnit 沒BUG人之翼是單元測試
NO BUG MANビームは Python Script 沒BUG人的光束是腳本強測
アーdalaoの力 身につけた 將dalao之力 集於一身
正義のヒーロー 正義的英雄
NO BUGマン NO BUGマン 沒BUG人 沒BUG人