1. 程式人生 > 其它 >OO 第一單元總結

OO 第一單元總結

OO 第一單元總結

一、作業架構設計與分析

1、第一次作業

UML類圖

類的設計

  • Term 類用來儲存單項式(a * x ** b)。
    • coefficient 為係數。
    • exponent 為指數。
  • Expression 類用來儲存表示式。
    • terms 為一個 HashMap,用來儲存單項式,該 HaspMap 以單項式的指數為 Key,單項式本身為 Value。
  • Lexer 類用於做詞法分析。
  • Parser 類用於解析表示式。

表示式解析

我採用遞迴下降的方法解析表示式。從結構上看,整個表示式被加減法分成了若干個項,每個項又被乘號分成若干個因子,因此,可以以遞迴的方式依次解析表示式、項、因子。

在解析表示式的時候,我本想分別定義 Expression、Term、Factor 類,採用遞迴巢狀的方式來儲存表示式,但是想不到一個比較好的合併同類項的方案,因此我轉化了思路,採用線性方式來儲存表示式,而在解析項和因子的時候,返回的都是一個表示式,這樣就可以直接跟原有的表示式進行合併,不必採用遞迴巢狀的儲存方式。這樣做也使得 Parser 類中只進行表示式層面的運算,而表示式層面的運算在 Expression 類中進一步的落實到底層的類。

合併同類項

當需要將一個單項式加入到表示式中時,只需判斷該單項式的指數是否有在 HashMap 的 KeySet 中,即可方便地進行同類項的合併。

2、第二次作業

UML 類圖

類的設計

  • 將第一次作業中的 Term 重新命名為 Monomial 用來儲存單項式。

  • TrigoFunc 類用來儲存三角函式。

    • type 為型別(sin 或 cos)。
    • content 為括號內的內容。
    • exponent 為三角函式的指數。
  • KeyOfTrigo 為三角函式去掉指數後的部分,用來判斷兩個三角函式可否通過乘法進行合併。

  • MulPart 表示一個單項式乘以若干個三角函式。

    • monomial 為單項式。
    • trigoFuncHashMap 是用來儲存三角函式的 HashMap,以 KeyOfTrigo 為 Key、TrigoFunc 為 Value。
  • KeyOfMulPart 是 MulPart 去掉常數係數以後的部分。

  • Expression 類用來儲存表示式。

    • mulPartHashMap 是用來儲存 MulPart 的HashMap,以 KeyOfMulPart 為 Key、MulPart 為 Value。
  • SumFunc 表示求和函式。

  • CuntomFunc 表示自定義函式。

    • type 為型別(f、g、h)。
    • numOfVars 為自變數個數。
    • variables 為自變數。
    • expression 為函式表示式。
  • Lexer 類為詞法分析器。

  • Parser 類用於解析表示式。

表示式解析

整體思路與第一次作業相同,對求和函式以及自定義函式的解析採用字串替換的方案。

合併同類項

(1)三角函式乘法合併

兩個三角函式能夠通過乘法合併的條件為除指數以外的部分均相同,因此建立上文所述的 HashMap 來儲存三角函式,當一個 MulPart 乘以一個三角函式時,只需判斷該三角函式對應的 Key 是否在 HashMap 中即可方便的進行合併。

(2)MulPart 加法合併

兩個 MulPart 能夠通過加法合併的條件是係數不同。同理,可建立對應的 HashMap。

表示式化簡

我在作業中只做了三角函式平方和的化簡,方案如下:

對於 Expression 類,遍歷 HashMap 的 Values,若遇到某一個 MulPart 含有 sin 或 cos 的平方,則將其替換為與之對應的可以通過平方和合並的三角函式(sin 替換成 cos,cos 替換成 sin),得到一個配對的 MulPart,接著判斷這個 MulPart 的 Key 是否有在 HashMap 中,如果有,則可將兩個 MulPart 進行合併。

下面舉個例子:對 2*sin(x**2)**3 + sin(x**2)*cos(x**2)**2,我們對其進行遍歷。第一項 +2*sin(x**2)**3 不含有平方項,跳過。第二項 +sin(x**2)*cos(x**2)**2 含有平方項,因此我們將 cos(x**2)**2 替換為 sin(x**2)**2 ,得到 +sin(x**2)*sin(x**2)**2,即 +sin(x**2)**3。此時我們發現 +sin(x**2)**3 的 Key 在原表示式中存在,對應 +2*sin(x**2)**3,因此我們將 +2*sin(x**2)**3 與 +sin(x**2)*cos(x**2)**2 進行合併,得到 sin(x**2)**3 + sin(x**2)。

3、第三次作業

UML 類圖

類的設計

整體設計與第二次作業相同,修改了 TrigoFunc 的 content 屬性為 Expression 型別。

表示式解析與化簡

整體方案與第二次作業相同。由於意識到了字串替換可能帶來的問題,我修改瞭解析自定義函式的方法,不再採用字串替換,而是對函式進行建模,將函式呼叫的實參逐個代入函式表示式。

4、優缺點分析

表示式解析

  • 優點:不採用遞迴巢狀而採用線性的儲存方式便於對錶達式進行解析和運算。
  • 缺點:難以建立一個統一的父類來管理所有因子,抽象程度不夠高。

表示式化簡

  • 優點:能夠快速查詢能夠進行平方和合並的項,效率較高。

  • 缺點:僅進行一次線性掃描,無法合併徹底,可能出現下面這種情況

    對於 sin(x)**2 + cos(x)**2*sin(x)**2 + cos(x)**4,cos(x)**2*sin(x)**2 與 cos(x)**4 合併成 cos(x)**2 後本應可以跟 sin(x)**2 繼續合併,但由於我們沒有對合並得到的 cos(x)**2 進行掃描,因此無法使之與 sin(x)**2 進一步合併。

二、程式碼度量分析

第一次作業

class OCavg OCmax WMC
Expression 2.0 4.0 22.0
Lexer 3.0 8.0 12.0
MainClass 1.0 1.0 1.0
Parser 2.2857142857142856 4.0 16.0
Term 1.6 5.0 16.0
Total 67.0
Average 2.0303030303030303 4.4 13.4

第二次作業

class OCavg OCmax WMC
expression.CustomFunc 2.5 6.0 10.0
expression.Expression 2.4615384615384617 9.0 32.0
expression.key.KeyOfMulPart 2.0 3.0 6.0
expression.key.KeyOfTrigo 1.6666666666666667 3.0 5.0
expression.Monomial 1.6666666666666667 5.0 20.0
expression.MulPart 1.8421052631578947 6.0 35.0
expression.SumFunc 4.0 4.0 4.0
expression.TrigoFunc 1.5714285714285714 4.0 11.0
Lexer 2.875 9.0 23.0
MainClass 2.0 2.0 2.0
Parser 1.9375 7.0 31.0
Total 179.0
Average 2.057471264367816 5.2727272727272725 16.272727272727273

第三次作業

class OCavg OCmax WMC
MainClass 2.0 2.0 2.0
expression.SumFunc 5.0 5.0 5.0
expression.key.KeyOfTrigo 1.6666666666666667 3.0 5.0
expression.key.KeyOfMulPart 2.0 3.0 6.0
expression.CustomFunc 2.2 4.0 11.0
expression.TrigoFunc 1.7777777777777777 6.0 16.0
expression.Monomial 1.5714285714285714 5.0 22.0
parser.Lexer 2.875 9.0 23.0
parser.Parser 1.8823529411764706 7.0 32.0
expression.Expression 2.111111111111111 9.0 38.0
expression.MulPart 1.8333333333333333 6.0 44.0
Total 204.0
Average 1.9805825242718447 5.363636363636363 18.545454545454547

分析

從分析結果可以看到,SumFunc 類的複雜度較高,這是因為再 SumFunc 中,我把求和函式呼叫的過程全都集中在了一個方法當中,使得整個類的複雜度偏高。

三、測試方案

由於指導書中已經給出了很詳細的表示式文法,因此可以採用遞迴的方式生成表示式,再利用 python 的 sympy 庫來比對自己的輸出是否正確。

在生成表示式的過程中,為了使其有較大的概率生成特殊的因子(比如 sin(0)),我先採用一個隨機數決定是否生成特殊因子,再進一步採用隨機數來生成因子。例如下面是我生成三角函式指數的程式碼,由於我做了平方和的化簡,因此要使其有較大概率生成平方項,以測試我平方和化簡的正確性。

if (Math.abs(random.nextInt()) % 2 == 0) {
    sb.append("** 2");
} else {
    sb.append(generateExponent());
}

四、bug 分析

1、自身 bug 分析

對常數的錯誤處理而產生的 bug

我在處理表達式時,一遇到 sin(0) 就將其轉化為 0,而對於 cos(0),我在輸出的時候才將其轉化為 1,這就導致在合併平方和的時候,若遇到 cos(0)**2,會尋找是否有 sin(0)**2 與之配對,而 sin(0)**2 又被我轉化成了零,因此就錯誤地使 cos(0)**2 與表示式中的任意常數項均能配對,從而使化簡出錯。

對於 bug 的修復,僅需對 cos(0) 採用與 sin(0) 相同的處理方式即可,即一遇到 cos(0) 就將其轉化為 1。而對於其他的三角函式,我並沒有採用特殊的處理方式,因此就不會再產生與常數配對的情況。

沒有認真閱讀指導書而產生的 bug

指導書中寫道,0**0 等於 1,求和函式的下界可以大於上界,我由於沒有注意到這兩點而產生了 bug。

2、他人 bug 分析

  1. 字串替換帶來的 bug :暴力地將 x*1 替換為 x,i**2 替換時沒有考慮到 i 可能為負數。
  2. 表示式化簡考慮不全面。
  3. 採用 int 儲存數字而造成溢位。
  4. 沒有認真閱讀指導書而產生 bug :0**0,sum 的上下界。

五、心得體會

1、面向物件與面向過程

pre 和 第一單元的學習讓我慢慢體會到了面向物件與面向過程思維方式的不同。以前寫程式的時候,我總是將一個問題分成若干個步驟,通過一步一步地完成每個步驟而解決這個問題。而在學習了面向物件之後,我拿到一個問題,首先考慮的是解決這個問題我需要哪些東西,各個東西需要有什麼功能、屬性,他們之間又該如何分工協作以解決這個問題。比如我需要寫字,那麼我需要有紙和筆,筆具有出墨的功能,紙具有粘墨的功能,他們共同協作以完成寫字這件事。

2、架構設計

在第一單元的迭代開發中我也深刻體會到了好的架構帶來的好處。好的架構易於拓展,當拿到一個新的需求,我們不需要對原有的程式碼做出大幅度的改動。同時好的架構也更容易 debug,若各個模組之間的耦合度比較低,那麼我們就可以通過 print 的方式快速的定位到 bug 的位置。