1. 程式人生 > 其它 >BUAA-OO-Unit1總結

BUAA-OO-Unit1總結

1 hw1

1.1 思路

1.1.1最初的想法

簡單劃分為Expr、Term、Factor三個層次,對每個層次建立一個類,例項化計算介面,對字串做遞迴下降,失敗 -> 忽視了表示式也可以是一種因子的邏輯關係,構建合併化簡方法困難。

1.1.2 重構

參考Training範例及討論區,建立Factor介面,對每種Factor單獨建類例項化介面;將計算方法打包建成Poly類。Expr是Term的"疊加",Term是Factor的"累積",Factor是一個或多個基元ax^b的"疊加"。

1.1.3 合併

希望可以獲得儘可能簡潔的形式。

ax^b的形式最簡潔了。考慮Term能否化簡成該形式。若Term可以化成ax^b的形式,則Expr就是\Sigma ax^b,則Factor是ax^b\Sigma ax^b,若Term中出現一個\Sigma ax^b的Factor,那麼Term就不能保證一定可以化簡成ax^b的形式。由上知Expr顯然不可能。所以必須接受Term是\Sigma ax^b,這樣Expr也是\Sigma ax^b,Factor可以是ax^b\Sigma ax^b,這樣Factor的乘積Term依然可以化簡成\Sigma ax^b

接下來構建對應的合併與化簡方法即可,主要包括:將三個層次類分別轉化成Poly(賦予計算能力),寫Poly(ax^b轉化而來)之間的乘法mult和ArrayList<Poly>\Sigma ax^b轉化而來)的加法plus和乘法mult,以及對ArrayList<Poly>去重化簡的unique方法。每次計算後,都要進行一次unique來避免化簡不徹底對後續計算產生的不便。

感謝強生同學在討論區的分享,讓我少走了很多彎路。

unique方法只涉及同類項的合併。只要將ArrayList<Poly>中的元素取出,String s = "x**"+poly.getExp()當Key、元素本身做Value,放入HashMap<String, Poly>中,邊放邊合併同Key的Value即可。這裡,使用不可變物件String做Key是一種比較安全的偷懶做法,在之後的作業裡有部分同學使用了HashMap<Factor>做Key,需要重寫equals、hashCode方法。但是用String有小瑕疵,我們會在後面

看到。

1.1.4 連續符號的處理

出於對正則的厭惡,預處理僅去掉了空白字元,未對多個-+相鄰的情況做處理。因而在parse時遇到了困難:表示式的符號性如何傳遞?

注意到遞迴下降實際上構建出了一棵以Factor為葉節點、Term和Expr為分支節點的表示式樹,且本層的負號僅影響下一層的節點,想到線段樹lazy標記。通俗的想法是:一方面,在本層級檢測下一級的符號,若為 '-' 則將下一級isNegative置1;另一方面,若本級isNegative標記為1,則僅在需要返回上級時將下層基元全部取反。具體操作,如下圖所示:

畫起來太麻煩了,直接放程式碼吧

    public boolean checkPosNeg(boolean isNegative) {
       if (lexer.peek().equals("-")) {
           lexer.next();
           return !isNegative;
      }
       if (lexer.peek().equals("+")) {
           lexer.next();
      }
       return isNegative;
  }

   public Expr parseExpr(boolean isNegative) throws Exception {
       Expr expr = new Expr();
       if (lexer.checkPrePosMinToken()) {
           expr.addTerm(parseTerm(checkPosNeg(false)));
      } else {
           expr.addTerm(parseTerm(false));
      }
       while (lexer.peek().equals("+") || lexer.peek().equals("-")) {
           expr.addTerm(parseTerm(checkPosNeg(false)));
      }
       expr.setPolys(Poly.unique(expr.polynize()));
       if (isNegative) {
           expr.negate();
      }
       return expr;
  }

   public Term parseTerm(boolean isNegative) throws Exception {
       Term term = new Term();
       term.addFactor(parseFactor(checkPosNeg(false)));
       while (lexer.peek().equals("*")) {
           lexer.next();
           term.addFactor(parseFactor(false));
      }
       term.setPolys(Poly.unique(term.polynize()));
       if (isNegative) {
           term.negate();
      }
       return term;
  }

parseFactor類似,比較長放不下。

1.1.5 輸出

由於將Expr視作Term的加法,故可能出現x+-x^2+x^3這種情況,雖符合題目要求,但不夠簡潔——輸出結果前消去多餘的+即可(向正則低頭)。

1.1.6 小困惑

遺留下了一個困惑:Number、Power、Expr都“是”一種Factor,為什麼選擇Factor介面而不是Factor抽象類?

2 hw2

2.1 任務

支援自定義函式、求和函式、三角函式。

2.2 實現

2.2.1 三角函式

先從三角函式入手,因為自定義函式、求和函式都與三角函式有關聯。

加入三角函式Triangle後,此時的數學基元應當是ax^b\Pi sin(factor)cos(factor)

Triangle本身自帶四個屬性:type、factor、exp、negative。

type factor exp negative
sin Or cos Factor型別 指數 標註符號

 

對Triangle建模後,相應地完善Poly計算方法和化簡方法即可。值得一提的,由於某種神祕力量HashMap自動按Key的hash值對entry排了序,儲存三角函式的HashMap也不例外;所以加上Triangle的基元依然可以利用字串來去重、合併——只需要遍歷基元Poly的三角函式容器,拼接到x**後面即可得Key。現在讓我們來填上前面埋的坑:測試中發現極少數情況下,合併失效,如樣例sin(x**2)*cos(x**2)-sin(x)+cos(x**2)*sin(x**2)會被原樣輸出。猜測與HashMap擴容時會反向儲存有關,而執行和除錯的初始容量並不相同。

 

 

 

2.2.2 自定義函式和求和函式

建立FuncFactory,表示式中遇到函式就扔到裡面,拿出處理好的表示式。這裡借鑑了研討課同組石子瑄同學的思路,特此致謝。具體實現中,分別建立自定義函式和求和函式的內部類即可。

工廠內部:原始表示式字串 ==> 做了字串替換後的表示式串 ==> parseExpr。

為了預防可能的迭代,架構支援各種函式名、各種引數名和引數個數的自定義函式,也可應對sum套sum的情況。

2.3 容易踩坑的點

一是實參的代入順序。對三個形參分別replaceAll容易導致剛把形參y全換成實參x**2,接著又把x全部換成x**2。我的辦法是手工正則匹配[param1|param2|...|paramn],邊遍歷邊做實參代換,只掃一遍表示式。(再次向正則低頭)

二是sum解析中正則錯誤地將所有的變元i全部換成數字,產生"sin(x) --> s1n(x)"的bug。穩妥的做法依然是手工匹配,在Lexer類中寫入對迴圈自變數的檢測即可。

3 hw3

本來以為是一次可以擺了的作業,看一眼指導書,發現三角函式裡面只能是"因子"……也就是說,如果裡面是個表示式,還需要額外加一層括號把它包起來。

經過化簡,三角函式的因子只可能是以下四類:Number,Power,Triangle和Expr,在toString前檢測下里面到底是個什麼東西就行了。我的做法是分別把裡面當做前三類parse一下試試看,如果均產生異常,那麼就是Expr了,給它加上括號。

這裡我陷入了面向過程的誤區,用了try...catch結構的巢狀來省時間,導致圈複雜度上去了,而且可能只有原作者能看懂在幹什麼,不利於團隊開發及維護。如果用三個boolean值對三次獨立try...catch的結果進行標記,再位運算,應該更“面向物件”一些。

4 基於度量的分析

Metrics

藉助了idea外掛MetricsReloaded。

由於三次作業架構保持了連續性,所以直接放最後一次的吧。

圖1 method分析

方法的平均CogC、ev(G)、iv(G)、v(G)都比較低,體現了高內聚低耦合的設計思想。

圖2 class的分析

Parser、Poly、SigmaFunctory類的OCavg(平均圈複雜度)和WMC(類總圈複雜度)較高,原因是這三個類中承載瞭解析、計算和展開的工作,特別是SigmaFuncotry還涉及到對展開的式子單獨進行Parse,任務較重。

UML類圖

以下分別是前三次作業UML類圖。

 

其中,對於每個類的解釋以Text Box的形式附在了UML圖中。在前三個小節也有對我認為比較重要的類或方法的說明,為節約版面不再複述。

優點:儘可能的根據數學語義解構表示式,直觀,便於維護。

缺點:Factory承載了太多,導致圈複雜度略高,應該還可以繼續拆解。

5 bug分析

hw1:交了一個半成品,強測爆零,很多功能在hw2中才得到了完善。

hw2:強測均過,互測未測出bug。

hw3:強測均過,互測測出兩個bug,均為細節上的問題。側面反映出細節決定成敗方法要與需求同步迭代。

第三次作業sum中可以出現"i**2"的情況,直接代入數字無法解析,需要在數字兩邊加括號。

另一個bug是在isPower的判斷中忘記-x也是表示式因子,程式出現sin(-x)的錯誤。

兩個bug在4行內同時得到了修復。

6 hack策略

從個人經驗來看,越小的資料越容易出問題。用常見的邊界條件手造一些小資料效果還不錯。

7 架構過渡

三次作業中架構保持了連續性,迭代開發很方便,沒有經過重構。其中,第一個架構向第二個架構過渡中新增了Triangle、CustomFunc、Sigma因子,並在計算類Poly中新增了有關Triangle的處理。第二次向第三次過渡,只在Triangle類中加入了對因子的判斷,及Factor中加入了輸出前用到的toStringFinal方法。

第一次作業經歷了一次不小的重構,直接導致時間不夠,迫不得已交了個半成品上去,結果自然是G。後面看,重構不可避免,越早重構迭代越順利

提前為下次迭代留下介面,事半功倍。

8 心得體會

我們在上面向物件,到底什麼是面向物件?沒有人給我們準確的定義。只能自己體會。

面向物件是程式設計師觀察並解析世界的一種方式。天上飛的、水裡遊的、地上跑的,大到宇宙、小到原子——任何事物都可以是物件。從現實需要出發,前人構建了繼承、介面等關係,構建了工廠、代理等設計模式。體會到一切從現實出發,設計架構就很容易了。