1. 程式人生 > 其它 >BUAA_2022面向物件_第一單元總結

BUAA_2022面向物件_第一單元總結

BUAA_2022面向物件_第一單元總結

O、寫在前面

總的來說,第一單元儘管涵蓋了面向物件這個名詞的大多數含義,但事實上其難度跨度並不大,更多的是給予我們一定的時間來適應這門課的一些特徵。根據我自己的理解,這門課最為重要的兩個特徵為:合作共贏、崇尚設計。

合作共贏:與該詞相對的是零和博弈,但實際上更貼切的是閉門造車。面對這樣一門新的專業課,大部分人都不敢自信的說自己凌駕於課程之上,我們的知識面小而散,很多時候需要助教加以指導才能逐步搭建其自己的成果。那麼合作在此扮演什麼角色呢?積沙成塔,積流成河。也許每個人的知識面都只是一小部分,每個人的想法都只在某一個方面擁有亮眼之處,但是通過交流就能將點子匯聚成一個完整的設計,這對一個難以上手的任務來說是彌足珍貴的。

崇尚設計:當我們在強調層次架構的時候我們在強調什麼?其本質,還是設計。面向物件以物件為單位,與之相隨的有八大設計原則,其難點在此,但其魅力亦在此。如何減少耦合性,如何滿足單一職責、開閉原則等設計理念是至關重要的,不論是漏洞查詢,還是程式拓展,亦或是區域性重構,都需要一個靈活明瞭的架構。往往從一開始的第一次作業,就決定了本單元的艱難與否,這與架構是分不開的。

一、關於三次作業

為何對上述兩個特徵印象如此深刻,那必定是我吃過這倆的虧。在第一次作業時,嘗試採取遞迴下降的方式進行解析,但是由於“沒想好就動手”,在倒數第一天遇到de不出來的bug,四處翻找也沒能在雜亂的設計中找到一個合理的bug解釋,無奈只能臨時跳轉預解析。另外,也是犯了閉門造車的老毛病,遞迴下降的演算法沒有理解透徹,寫的時候只是根據淺顯的認識加以模仿,導致後續debug的時候沒有一個明確的思維指引。而之後,權衡其他課程的時間,終究沒能選擇重構。

1.1 儲存方式選擇

言歸正傳,既然走上預解析的不歸路,那就暫且不談解析方法,而專注於儲存方式與化簡技巧吧。下面附上最終三次作業的UML圖。

對於儲存方法,廣泛使用的有兩種:尋求一般形式、(遞迴)建樹。下面對這兩種進行詳盡的分析。

1.1.1 尋求一般形式

這對於第一次作業是再簡單不過了,沒有sin和cos的干擾,我們需要考慮的範圍僅僅是多項式環F[x]。在該範圍中,一個多項式可以僅僅用指數、係數構成的節點連結而成,於是很容易想到利用一系列帶兩個屬性:coefficient、exponent,的term來儲存整個多項式結構。對於簡單的合併操作,加法比較exponent然後操作coefficient,乘法coefficient相乘、exponent相加,幾乎沒有任何難點。

第二次作業增加了sin和cos,如果在尋求一般形式,則形式如下:

$$
f(x)=a*x^b*sin^{m_1}*sin^{m_2}*...*cos^{n_1}*cos^{n_2}*...
$$

仍可以採用單個term的形式存取這一個一般形式。其優點顯而易見,即架構尤其簡單,只需要將term進行相加即可;其缺點也非常明顯,即在這一個term類中,需要實現x、sin、cos這三者的計算操作,這直接引起討論的情況數大大增加,不論是程式碼量還是出bug概率都大幅提升。

如果拿單一職責與開閉原則來比對,由於一個類實現幾乎全部計算,顯然不滿足單一職責;另外,三角函式和多項式項集合在一起,如果另加其他函式,則需要在每種分支結構中新增新的分支,顯然也不滿足開閉原則。因此,這樣的構思是完全面向問題設計,如果再繼續拓展下去,重構不可避免。

1.1.2 (遞迴)建樹

至於為什麼在遞迴外加括號,想必大家也很清楚。可以說,在第三次允許巢狀之後,遞迴儲存的方法成為“首選”方法。運算式本身就是一個多層巢狀結構,其本質,也是一個類似b+樹的結構:非葉子節點——多元運算子;葉子結點——基本運算單位。因此只要這個樹建好了,就可以通過後序遍歷,遞迴進行:獲取操作物件(子節點),獲取操作符(本節點),得到本節點的運算結果,最終就能在根節點獲取全部的運算結果。

第一單元作業全部採用BNF形式化描述,其遞迴描述的方式為我們直接提供了遞迴儲存的思路。於是很容易就能構建出上面UML圖的架構:頂層formula表徵只定義了加法的多項式,子類term表徵只定義了乘法的多項式,而factor則表徵乘法的單元物件,即a*x^b與三角函式,而在三角函式中又用content儲存頂層的formula,由此構成遞迴儲存結構。

這樣儲存的優點:具有靈活性和可拓展性,並且對化簡友好。相較於一般形式的方法,遞迴儲存方式將表示式結構劃分為一個個小的要素,可以分別定義各要素之間的運算,這樣為履行單一職責提供了天然條件;另外對要素進行了分類,因此新增其他運算單位時可以作為新的類,並繼續定義此類與其他運算單元的互動方法(運算)。有關化簡下面會談到。

1.2 複雜度分析

下面是使用idea圈複雜度分析外掛MatricsReloader的分析結果

aim.Formula 4.384615384615385 10.0 57.0
aim.Term 3.8461538461538463 13.0 50.0
aim.Parser 3.75 12.0 30.0
aim.Variable 2.1 8.0 21.0
Main 2.0 2.0 2.0
aim.Triger 1.8181818181818181 6.0 20.0
aim.Factor 1.1666666666666667 3.0 14.0
Total     194.0
Average 2.8529411764705883 7.714285714285714 27.714285714285715

這是排序後的類複雜度評估結果,可見,Formula類由於幾乎所有運算都需要從該類開始,並且下述的幾個主要方法都以該類為頂層展開,Term緊跟其後,可以說,有這樣的結果是不言而喻的。

aim.Term.toString() 38.0 5.0 16.0 16.0
aim.Formula.simplify() 21.0 1.0 9.0 10.0
aim.Parser.parser() 15.0 1.0 8.0 12.0
aim.Term.trigerCompose(Term) 14.0 1.0 10.0 10.0
aim.Formula.add(Formula) 13.0 5.0 8.0 9.0
aim.Formula.equals(Formula) 12.0 7.0 5.0 8.0
aim.Term.addAbility(Term) 12.0 7.0 4.0 8.0
aim.Formula.need() 11.0 6.0 8.0 10.0
aim.Triger.toString() 11.0 3.0 6.0 6.0
aim.Variable.toString() 11.0 4.0 7.0 8.0
aim.Formula.add(Term) 10.0 4.0 6.0 6.0
aim.Term.simplify() 9.0 3.0 7.0 7.0
aim.Parser.isTerm(String) 8.0 5.0 6.0 7.0
aim.Term.multiply(Term) 8.0 4.0 5.0 5.0
aim.Factor.equals(Factor) 7.0 3.0 3.0 5.0
aim.Formula.multiply(Formula) 7.0 2.0 5.0 6.0
aim.Triger.multiAbility(Factor) 6.0 4.0 4.0 4.0
aim.Formula.toString() 5.0 4.0 5.0 5.0
aim.Formula.addUp() 4.0 1.0 4.0 4.0
aim.Parser.getTwoOperator(String[]) 4.0 1.0 5.0 5.0
aim.Triger.composeAbility(Factor) 4.0 1.0 7.0 7.0
aim.Variable.tackle(String) 4.0 1.0 4.0 4.0
aim.Term.hasTriger() 3.0 3.0 1.0 3.0
aim.Triger.equals(Factor) 3.0 2.0 5.0 5.0
aim.Variable.equals(Factor) 3.0 2.0 3.0 3.0
aim.Parser.computeNeg(String[]) 2.0 1.0 2.0 2.0
aim.Parser.computePos(String[]) 2.0 1.0 2.0 2.0
aim.Parser.computeTriger(String[]) 2.0 1.0 2.0 2.0
Main.main(String[]) 1.0 1.0 2.0 2.0
aim.Factor.myClone() 1.0 1.0 2.0 2.0
aim.Formula.myClone() 1.0 1.0 2.0 2.0
aim.Formula.neg() 1.0 1.0 2.0 2.0
aim.Term.equals(Term) 1.0 1.0 2.0 2.0
aim.Term.myClone() 1.0 1.0 2.0 2.0
aim.Factor.Factor() 0.0 1.0 1.0 1.0
aim.Factor.composeAbility(Factor) 0.0 1.0 1.0 1.0
aim.Factor.getCoefficient() 0.0 1.0 1.0 1.0
aim.Factor.getContent() 0.0 1.0 1.0 1.0
aim.Factor.getExponent() 0.0 1.0 1.0 1.0
aim.Factor.getType() 0.0 1.0 1.0 1.0
aim.Factor.multiAbility(Factor) 0.0 1.0 1.0 1.0
aim.Factor.setCoefficient(BigInteger) 0.0 1.0 1.0 1.0
aim.Factor.setExponent(BigInteger) 0.0 1.0 1.0 1.0
aim.Factor.toString() 0.0 1.0 1.0 1.0
aim.Formula.Formula() 0.0 1.0 1.0 1.0
aim.Formula.addTerm(Term) 0.0 1.0 1.0 1.0
aim.Formula.peek() 0.0 1.0 1.0 1.0
aim.Parser.Parser(List) 0.0 1.0 1.0 1.0
aim.Parser.makeFormula(String[]) 0.0 1.0 1.0 1.0
aim.Term.Term() 0.0 1.0 1.0 1.0
aim.Term.addFactor(Factor) 0.0 1.0 1.0 1.0
aim.Term.getCoefficient() 0.0 1.0 1.0 1.0
aim.Term.getFactors() 0.0 1.0 1.0 1.0
aim.Term.setCoefficient(BigInteger) 0.0 1.0 1.0 1.0
aim.Triger.Triger(String, Formula) 0.0 1.0 1.0 1.0
aim.Triger.getCoefficient() 0.0 1.0 1.0 1.0
aim.Triger.getContent() 0.0 1.0 1.0 1.0
aim.Triger.getExponent() 0.0 1.0 1.0 1.0
aim.Triger.getType() 0.0 1.0 1.0 1.0
aim.Triger.setCoefficient(BigInteger) 0.0 1.0 1.0 1.0
aim.Triger.setExponent(BigInteger) 0.0 1.0 1.0 1.0
aim.Variable.Variable(BigInteger, BigInteger) 0.0 1.0 1.0 1.0
aim.Variable.Variable(String) 0.0 1.0 1.0 1.0
aim.Variable.getCoefficient() 0.0 1.0 1.0 1.0
aim.Variable.getExponent() 0.0 1.0 1.0 1.0
aim.Variable.multiAbility(Factor) 0.0 1.0 1.0 1.0
aim.Variable.setCoefficient(BigInteger) 0.0 1.0 1.0 1.0
aim.Variable.setExponent(BigInteger) 0.0 1.0 1.0 1.0
Total 255.0 123.0 203.0 225.0
Average 3.75 1.8088235294117647 2.985294117647059 3.3088235294117645

經過排序可以發現,複雜度最高的一個是term的toString方法,這是由於優化輸出的需要,必定會在toString中進行完備的討論。譬如,係數為0或1的情況,指數為0或1的情況,有三角函式或沒三角函式等等,這樣一來,巢狀if的層數增多,加上factor的複雜度,才導致如上結果。另外,我每次呼叫化簡方法simplify都是在toString前呼叫,因此也會增加其複雜度。排名第二的formula的simplify(化簡)方法,也是由於多次遍歷元素導致的複雜度上升。除此之外,圈複雜度高的方法基本都是“判斷類”方法,這類方法需要進行大量比較,對於第二種儲存方式,由於其“深度”加深,因此在遍歷比較的時候需要多層巢狀的迴圈,這自然也是其複雜度高的原因。

因此在設計的時候儘可能減少if巢狀與迴圈巢狀是減少圈複雜度的關鍵方法。

1.3 bug分析

按理來說,預解析是不該有bug的,但是這動手比腦子塊的老毛病讓我在寫程式碼的時候總是“先下手為強”,最終不得不“拆東牆補西牆”,下次一定多多注意。

前兩次作業我確實採取第一種儲存方式,第一次由於提交匆忙,有一處的String轉Biginteger仍採用的是long過渡的形式,導致這一個bug被六個人圍攻,著實不該,此處姑且不談。著重談談第二次作業遇到的天坑。

1.2.1 淺克隆與深克隆

慚愧的是,在我寫第二次作業之前,就有了解討論區吳佬發起的討論,但是在實際運用的時候,還是由於理解不深導致致命bug。沿用了第一次的架構,我一直都沒有考慮這個問題。加法乘法運算,我一開始採用的是自儲存的方式,即

$$
A.add(B),\ A.multiply
$$

這兩種計算的結構都是直接修改A,之前在作業一中,由於Biginteger的不可變特性,這個漏洞沒有顯現,但是在作業二中操作物件變為定義的類,因此在多次進行上述操作的時候,這致命的bug才會逐漸浮現。可以看出,這個bug的關鍵在於,如果A儲存結果,那麼B必須不可變,即除了方法內不能更改B,上述運算形式還必須具有方向性,絕對不可以B.multiply(A)

針對這個bug,我在第三次作業重構中採取:每種運算都用一個新的物件來儲存。這對加法而言好說,但是乘法還是自儲存更為方便(畢竟依據一個個factor重新設定term著實沒必要),因此我採用先將A進行克隆,然後再與B進行乘法,將結果加入到另一個預先設定好的容器中。對於clone,我是通過在各類中統一定義了myClone方法,利用Serializable介面利用序列化克隆實現的。

1.2.2 toString

如果採用第一種儲存方式,那麼toString一定需要小心,因為如果只定義了term、並且還有化簡想法的我們,就會如上面複雜度分析那般所言。眾多if巢狀就必須考慮全面,否則就會出錯。另外一個bug集中的地方是化簡的部分,提交最後一版的時候由於化簡演算法不夠成熟,存在些許bug而最終選擇簡單化簡的一版。從這裡可以看出,複雜度更高的模組,其出bug的概率也越大,上面分析到的其很大一部分原因是由於大量if巢狀與迴圈巢狀,這正是我們debug需要注意的地方。

在某些方面來看,在term中toString,不如分散到factor中進行toString,這也是第三次作業重構的一大原因。

1.4 化簡分析

第三次作業著重化簡,我的架構在上面的UML圖中十分顯然,之前也說過這樣的架構對化簡友好,這裡就詳細談談其優點。

化簡的關鍵點有二:能否化簡、如何化簡。

我的設計中將二者分開實現,“能否化簡”用\#Ability的形式定義方法,而化簡操作,則直接在toString中呼叫。

1.4.1 簡單長度化簡

對於基本的長度化簡此處做簡單概括。

首先是合併操作,加法的係數合併,乘法的指數合併,以及有0的時候的消除合併操作。當然,對於x**2轉化為x*x就不必多說了。需要說明,這裡需要考慮到消除合併時,子類列表為空的情況,譬如formula的terms列表裡,由於係數全消為0而全被移除列表,導致terms.size()==0,這種情況需要特判。

其次是順序移動操作,講正數提到表示式開頭可以省去一個符號的長度。

最後是三角函式括號的化簡,對於有符號整數與x冪次的情況,sin和cos中不需要兩層括號的,這可以通過一個need函式判斷是否要加括號來實現。

1.4.2 三角函式合併化簡

三角函式的平方和:我的做法是,在formula層級遍歷term,尋找每對term中可能可以相加的指數為2的三角函式,利用一個composeAbility方法進行判斷,如果可以,則對sin邊消除sin^2成分,而對cos這一邊,係數需要減去sin那一邊的係數。

二倍角操作:在單個term中進行搜尋,同樣記錄可能可以合併的三角函式,利用doubleAbility方法進行判斷,如果可以,則消除cos成分,並令係數除以2(前提是係數為2的倍數,因為本次作業不支援小數),令sin內content的係數double。

二、關於hack經歷

只能說從提心吊膽到心平氣和。最初對自己的設計確實有所不自信,一方面來源於對java語言本身的不熟悉,擔心有語言特性上的bug,另一方面還是自己架構的設計問題,缺乏此類經驗,沒有想得特別清楚。從最開始被hack5、6次,到最後0/20的hack結果,不僅體現了程式碼架構邏輯的進步,也說明了我的設計方法信心的積攢。

對於我hack別人,第一次兢兢業業下載別人的程式碼進行閱讀,用資料驅動來發現存在的bug,然後針對bug進行hack。之後兩次的hack,都是藉助資料生成器,初此之外,加上自己能想到的邊界資料來進行測試。只能說,星期天需要處理的作業有點多,留給hack的時間著實不多。。。

總體而言,所遇見的bug集中在兩個方面:

  1. 擴充套件後的成分沒有考慮到老的特殊情況。譬如前導零、帶符號的指數、資料範圍(未用biginteger)等等,以及最終結果為0或1的輸出情況處理。

  2. 擴充套件後的乘法,即sin、cos分別存在的兩個多項式之間的乘法或乘方。一般來說這是由於合併的結果。

基本上,如果提交前在這幾個方面多多測試,在配以幾個較為複雜的測試樣例,在hack途中可能出現bug的概率不大(前提是架構明確。。。)

三、心得體會

在小組討論過程中,我們組尤其討論了撰寫部落格的意義所在,其中一大討論結果就是“積累經驗”。經驗一次看似一筆帶過般簡單,但事實上卻濃縮著整個大腦的抽象工作。它負責將我們所學的知識串聯起來,並抽象成一塊塊有關係有結構的樹型知識鏈,儘管日後我們不一定記得詳細的知識點,但是卻能聯想到這方面的主要思想和易出錯點,知識點忘了可以重新查詢,但是“經驗”在未來做專案時扮演者極其重要的角色。當然,這樣說並不意味著知識點不重要,能記住知識點固然更好,只不過我認為經驗更為重要。

經過這三週的學習,最大的收穫還是文前所言的兩個課程特徵,這也是課程對我們提出的要求。如何能在一週為數不多的大段時間內,從設計到實現一個解決問題的方案,不僅考驗知識,更是考驗能力。我們需要合作,我們不僅要有自己的想法,更要善於學習他人想法,並在此過程中改善自己的想法,這樣才能鍛煉出短時間內得出最佳方案的能力;我們需要想清楚再動手,這個階段才是最耗費時間的,如果將架構設計好之後,bug會少,重構會少,可謂一勞永逸。

希望下一單元能搞定這兩方面的問題!