第一次作業
設計思路:
第一次作業的主要要求為實作多項式的讀入,通過正規表達式拆分成各個power型項,最後進行求導輸出。由于要求的功能較為簡單,我隻設計了兩個類,分别是Word類來儲存每一項的系數與指數,Main函數類進行解析,求導,和輸出最終的結果。整個工程主要的難點在于讀入整行字元串時如何将其分成帶符号的各個項,下面是用于解析的正規表達式的效果。
正則解析:
UML類圖:
基于度量的程式結構分析:
通過IDEA下的Metrics Reloaded 插件進行複雜度分析和依賴度分析。
度量分析圖如下:
分析發現Main函數下的parse方法和其所調用的其他方法的聯系十分緊密,即耦合度較高且循環複雜度較高,這也是第一次作業中我 “ 1main到底 ” 的面向過程設計思路的弊端,也沒能較好的拆分方法,沒能較好的實作高内聚、低耦合的項目設計目标。
Bug分析:
我方 :
這次作業我在強測中沒有出bug,而在互測中經同學的Hack,發現了例如“-- x”等輸入,如果x與其前面的雙重符号中間存在空格,我将隻能識别到第一個符号的bug。出bug的原因在于我沒能仔細的閱讀指導書,沒能提前想到“-- x”的情況存在,修複時對字元串提前進行空格處理即可。
對方 :
互測時主要使用評測機對對方代碼進行測試,通過Python的xeger和sympy 庫進行随機的多項式格式生成與計算比較。主要重點部分如下:
def random_poly():
xe = Xeger(limit=20)
regex = r"[\t ]*([+\-][\t ]*)((((([+\-]?[1-9]+)[\t ]*[*][\t ]*)|([+\-]))" \
r"?(x([\t ]*([*]{2}[\t ]*([+\-]?[1-9]+)))?))|([+\-]?[1-9]+))[\t ]*"
ran_poly = str()
for j in range(50):
ran_poly += xe.xeger(regex)
return ran_poly
在測試中,發現兩位同學分别有:沒能正确使用BigInterger處理大數,沒能正确解析字元串中間出現的 “ -+ ” “ +- ” 等連續符号的問題。
第二次作業
第二次作業相較第一次作業主要添加了sin(x),cos(x)的輸入情況,以及在較為簡單的情況下判斷并輸出WF,相比于第一次作業在設計上則主要新增了sin,cos,pow,cons類,且四者分别繼承自Word,即項類。整體作業難度也在于正規表達式的解析,WF的判斷依據則根據兩次比對對應項的字元串中間是否存在非空格的字元來完成,這也是我認為比較新穎的地方。
分析發現Main Class和Mainex Class 的複雜度的仍然很高,雖然實作了繼承結構模式,但沒有合理的處理拆分方法與方法的位置,導緻問題發生,這些部分将在我的第三次作業中實作較為完善的優化。
這次作業在強測與互測中沒有Bug發現。
互測思路與第一次類似,通過構造随機數評測機進行測試,主要部分如下
def random_poly():
xe = Xeger(limit=20)
regex =r'[ \t]{0,3}([*][ \t]{0,3}|[+\-][ \t]{0,3}[+\-]?[ \t]{0,3})' \
r'(((cos[ \t]{0,3}\([ \t]{0,3}x[ \t]{0,3}\)' \
r'|sin[ \t]{0,3}\([ \t]{0,3}x[ \t]{0,3}\)|x)' \
r'([ \t]{0,3}(\*\*[ \t]{0,3}([+\-]?\d{1,2})))?)' \
r'|([+\-]?\d{1}))[ \t]{0,3}'
ran_poly = str()
for j in range(50):
ran_poly += xe.xeger(regex)
return ran_poly
在測試中,發現一位同學存在例如“ ++8*7* cos ( x ) ”等多個常數項相乘時,會自動忽略第一項的問題
第三次作業
第三次作業較前兩次作業有了難度上的極大提升,其中主要包括非規則的括号嵌套結構,正則比對的貪婪算法導緻對于例如“cos(x)*(x)“等類型的表達式不可直接解析,需要一定的預處理。
在設計上,解析算法層面,我選擇先對括号進行解析,從識别最内層" ( "," ) ",将其中的内容儲存到Hashmap中,再将該層的括号替換成" [ "," ] " (當然前提是在一開始讀入字元串時已經掃描過字元串,鑒别有無非法字元" [ "," ] " )。這樣處理就較好的解決遞歸終點判斷與正則解析時由于貪婪導緻的捕獲組不滿足要求的問題。
度量分析圖如下:
method | ev(G) | iv(G) | v(G) |
Cons.Cons(BigInteger,BigInteger,Term) | 1.0 | ||
Cons.printTerm() | |||
Cos.calcDer() | 4.0 | ||
Cos.clone() | |||
Cos.Cos(BigInteger,BigInteger,Term) | |||
Cos.printTerm() | |||
Main.getLists() | |||
Main.main(String[]) | 9.0 | 10.0 | |
Main.setLists(ArrayList<hashmap<word, word="">>) | |||
Nest.calcDer() | 2.0 | ||
Nest.clone() | |||
Nest.getNest() | |||
Nest.Nest(HashMap<word, word="">) | |||
Nest.printTerm() | 5.0 | ||
Nest.setNest(HashMap<word, word="">) | |||
Pow.calcDer() | 3.0 | ||
Pow.clone() | |||
Pow.Pow(BigInteger,BigInteger,Term) | |||
Pow.printTerm() | |||
Sin.calcDer() | |||
Sin.clone() | |||
Sin.printTerm() | |||
Sin.Sin(BigInteger,BigInteger,Term) | |||
Term.getCoe() | |||
Term.getIndex() | |||
Term.getInner() | |||
Term.setCoe(BigInteger) | |||
Term.setIndex(BigInteger) | |||
Term.setInner(Term) | |||
Term.Term(BigInteger,BigInteger,Term) | |||
Word.addNewTerm(Term) | |||
Word.doDerivative() | |||
Word.getMap() | |||
Word.mapSize() | |||
Word.printWord() | |||
WordMaker.consFind(Matcher) | 6.0 | ||
WordMaker.cosFind(Matcher) | 7.0 | ||
WordMaker.nestFind(Matcher) | |||
WordMaker.parse(ArrayList) | |||
WordMaker.powFind(Matcher) | |||
WordMaker.sign(String) | |||
WordMaker.sinFind(Matcher) | |||
WordMakerEx.getMap() | |||
WordMakerEx.judgeValid(StringBuilder) | |||
WordMakerEx.setMap(HashMap<word, word="">) | |||
WordMakerEx.WordMakerEx(StringBuilder) | |||
Total | 51.0 | 121.0 | 125.0 |
Average | 1.0625 | 2.5208333333333335 | 2.6041666666666665 |
通過與前兩次作業項目複雜度的對比發現,通過将求導計算,字元串parse識别等方法分别下放到Sin Cos等Class中,極大簡化了Main Class的複雜度,相較之前的一main到底有了較大的提升。并且在求導運算過程中,因子的符号與定位已經确定,不需要考慮因子間的關系,在運用鍊式求導法則時,因子間能互相完全獨立,達到解耦合的目的。
這次作業在強測與互測中發現的bug在于我處理例如 “ -- x “,” +- 1 “ 等應當被當做表達式的字元串時,由于沒有正确的解析中間的空格,程式會将其解析成正常的因子,最終導緻例如 ” sin(-- 1)“等錯誤的輸入無法輸出 ” Wrong Format!“
另外一個bug原因在于最初沒有對針對pow類型進行嵌套處理,導緻處理括号外面的符号時無法正确識别,例如sin((x-(x)))無法成功計算,修改時将原先沒有增加系數(預設為1)的nest類增加系數即可。
由于這次沒能很好的寫出覆寫完善的評測機,在hack部分便主要采用覆寫性測試的方法,針對該次作業的特殊情況,我的測試資料主要集中在一下方面:
(1)爆破tle和正則 : "((((((((((x))))))))))”
" ((((((((((((((x)*x)*x)*x)*x)*x)*x)*x)*x)*x)*x)*x)*x)*x) ”
" -x-(x-(x-(x-(x-(x-(x-(x-(x-(x-(x-(x-(x-(x-(x)))))))))))))) ”
(2)針對化簡坑點的測試 : “ cos((x*x)) “ “ sin(x)*(x) ” “ sin((++(-+(x)))) ”
(3)邊緣資料測試 : “ -(x)**50 “
建立模式:
設計的思路在前文每次作業的分析中已經提及,下面主要講講我自己在建立模式設計時的一點心得。
由于前兩次作業整體結構較為簡單,在一開始書寫代碼的時候,我沒有太注重提前的結構設計架構,可以說是完全按照面向過程的程式設計,結果化體系也隻展現在對于多項式的項這一個大類上面使用了工廠化思想,還是沒有擺脫一main到底的糟糕布局。在第三次作業中,由于本身多項式的解析複雜度大幅度上升,我曾嘗試使用第二次的建立模式作為模闆,結果在給sin,cos類添加cal方法進行求導時,發現判斷到遞歸終點時要引用多個Main裡的方法,即兩個類的V(G)指數較高,關聯度強,導緻耦合度極高,且繼承關系十分混亂,最後隻能含淚重構,保留了之前的sin cos cons pow類,新增wordmaker等專門對解析出來的項進行運算,再用term,nest類構造資料的儲存格式,進而達到解耦合的效果。
對比和心得
對比:
通過和老師助教們整理出來的優秀代碼作業進行對比,我發現很多優秀的代碼都是通過表達式樹的形式實作的,尤其是看了tyh大佬用一個 Lexer 轉成 token 表示,然後用 Shunting yard進行表達式解析的方法,真的學到了很多,比其我之前的超長正則捕獲來講,無論是可維護性還是可閱讀性都強了太多。還有lyj大佬寫的自動機與棧處理,也十厘清晰得當,相較我的直接一股腦丢進map裡要簡約美觀很多
心得:
第一個單元的作業總體感覺前兩次難度較低,到第三次難度有了較大的提高,由于前兩次我并沒很好的進行結構設計與整體的包裝設計,導緻第三次要求過程複雜後進行了大量的重構,這是我不太滿意的地方,希望在第二單元的學習中我能避免這個誤區,從第一次作業開始就好好的規劃,力求不再重構。另外一點便是要嘗試TDD思路,即在Coding之前先構造足夠且覆寫面較廣的測試集,這樣知己知彼,能避免發生後期發現忽略一種情況,但無法在不破壞整體結構的情況下補充進去的窘境。當然,因為我有着做很多事開始前十分猶豫,拖沓的惡習,是以對于我自己來講還是要權衡好構思的時間範圍,畢竟萬事開頭難,幹就完事了!