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

BUAA OO Summary - Unit1

Unit1

目錄

目錄

Task1架構

所謂萬事開頭難,當看到第一次作業以後,覺得很沒有頭緒:不知道怎麼解析字串,不知道是應該一遍解析表示式一遍進行化簡還是解析完了以後再化簡...總之就是,頂層架構無法確認,具體細節也不知道怎麼實現。於是在反覆閱讀指導書並且與朋友進行交流以後,才開始進入正式的寫程式碼階段。雖然後面終於有了一個可以實現的想法,但是心裡隱隱覺得,下週一定是要重構了。

首先巨集觀來看整個解題步驟,分為一下幾步

讀入處理

首先,當讀入字串後,由於本次作業不進行錯誤格式判斷,所以可以直接在這一步先把空白符全部去除,再進行下一步的表示式解析

package parser---解析表示式(用遞迴下降法構造表示式樹狀結構)

這裡沿用模仿了訓練程式碼中給出的Lexer類和Parser類

Lexer

Lexer用來掃描輸入的字串,並且匹配我們所需要的的token,其中的peek()方法可以讓我們獲取當前所讀到的token,而每當我們讀到某一個token併成功解析後,可以呼叫其中的next()方法,來繼續解析下一個token,直到整個表示式解析完為止,也就完成了整個表示式樹的構建

這裡支援獲取的token包含:['+','-','*','**','(',')',不帶符號的數字]

Parser

從形式化表述中可以清晰的知道,表示式由項通過加減號連線組成,而項由因子相乘組成,因子由變數因子、常數因子、表示式因子組成

所以這裡很自然分為parseExpression,parseTerm,parseFactor來從頂層整個字串逐個解析表示式

關於對符號的處理

這裡,參照形式化表述,為了統一處理,降低複雜度

  • parseExpression中對表示式的解析過程中,認為所有項都是由加號連線,而每一項前面的符號則歸併至該自身。這樣也統一了第一項前面有符號的情況。

    當遇到負號,就在parseTerm以後返回的Term中再加入一個值為-1的常數因子(最後會進行合併同類型和化簡處理),即相當於讓解析到的當前項再乘上一個-1的因子;

    而當遇到正號,我們就可以不做處理,直接把解析完的加入當前表示式就好

  • parseTerm中對進行解析時,參照形式化表述中,如果最前面的加減符號存在且為負號,則直接在這個項中加入一個值為-1的表示式因子,相當於整個項×-1

  • 而在parseFactor中,需要特殊處理的表示式因子,判斷如果掃描到'(',則parseExpression,即表示式因子;在解析完表示式因子後,如果遇到token'**',則記錄其後面的數值(注意這裡要記得處理指數的符號),記錄下來,然後在返回到parseTerm時,向該向中新增相應數值次的表示式因子

    比如:(x+1)**2,即向項中新增兩次(x+1),效果相當於(x+1)*(x+1)

package expression---表示式儲存架構

這裡設定了兩個介面,主要是為了不混淆term

  • Factor介面(顯然因為下面三個都是因子)
    • PowerFunc
    • Constant
    • Expression
  • Poly介面(裡面有一個getHashMap方法,其實下面四個都能看成一個多項式存進HashMap中,下面化簡表示式部分講述,只不過後三個看成該多項式只含有一個項,HashMap中的key表示指數,value表示係數)
    • Term
    • PowerFunc
    • Constant
    • Expression

化簡表示式

getHashMap()

從最底層因子類factors:冪函式、常數、表示式因子,得到他們對應的hashmap

比如對於冪函式x**2,其hashmap為<key: 2, value: 1>

對於常數1,其hashmap為<key: 0, value: 1>

對於表示式因子x**2+x+1,其hashmap為<key: 0, value: 1>,<key:1, value: 1>, <key: 2, value:1>

再在項類term,對這些因子的hashmap迭代相乘,逐漸拆括號,類似於當求(x+1)×(x+2)×(x+3),先得到(x2+3x+2)×(x+3)後,再拆一次括號為最終的(x3+6x^2+11x+6);最後得到的每個項都將是不含有表示式因子的項,即括號拆除完畢

得到term展開括號後的hashmap,最後在表示式類expression,把這些已經拆過括號的項再合併,達到最終的hashmap,即hashmap相加

最後在main裡面得到返回的hashmap這是就已經是合併同類項以後的最簡形式

最終只需呼叫expression裡面的toString方法,利用hashmap逐個打印出a*x**b形式的項的和即可

這裡的gethashmap實則也是遞迴下降

關於優化

為了使得最後輸出的結果在正確的前提下儘可能的短,最後在expression裡面的toString方法要進行一下特判與處理;

比如當指數為0時,,即hashmap的key為0時,不列印指數部分;當指數為1,不列印“**1”;

生成資料

用到了python的Xeger,利用正則表示式來生成最基本的常數因子和表示式因子

然後寫了幾個函式分別用來生成

  • 不含表示式因子的項
  • 不含表示式因子的表示式(即用來生成表示式因子)
  • 含表示式因子的項
  • 表示式

Task2架構

果不其然,先預告一下,真的重構了。不過,重構不可怕,因為有了好的架構,才能有利於後期更好的迭代開發,這也讓我在第三次作業嚐到了很大的甜頭。所以,一定要設計一個自己覺得至少不錯的架構,如果自己都不能和自己的架構和解,問題早晚會暴露出來

檔案樹

│  InputHandle.java
│  Main.java
│
├─expression
│      Adder.java
|	   Multiplier.java
│      PowerFunc.java
│      Constant.java
│      Sin.java
│      Cos.java
│      MyFunc.java
│      Sum.java
│      Standard.java
│      
└─parser
        Lexer.java
        Parser.java

關於重構

架構

​ 由於上理論的時候,老師不斷強調,對於例如1+2+3這樣的表示式,我們可以把1,2,3分別看成一個整體;也可以把1+2看成一個整體,3看成一個整體;最後1+2+3也可以看成一個整體.

​ 再結合指導書對於架構設計的建議:

  • 對於每一種函式(常數、冪函式、三角函式、求和函式、自定義函式),分別建立類
  • 對於每一種運算規則(乘法、加減法),分別建立類
  • 對於自定義函式,可以先將其 定義表示式 展開,將展開後的結果代入進行計算。
  • 對於求和函式,可以類似於自定義函式進行代入

​ 於是,我一共定義了8個函式類,分別為加法類(Adder)、乘法類(Multiplier)、冪函式類(PowerFunc)、常量類(Constant)、正弦函式類(Sin)、餘弦函式類(Cos),他們實現了Function介面

化簡

Adder類化簡核心程式碼(只包含主要邏輯)如下:
public Adder simplify() {
	for (Function func : functions) {
		if (func instance of Adder) {
			tmpFunctions = ((Adder) func).simplify().getFunctions();
			simFunctions.add(tmpFunctions);
		} else if (func instanceof Multiplier) {
			tmpFunction = ((Multiplier) func).simplify();
            for (Function func1 : tmpFunction.getFunctions() {
            	if (func1 instanceof Adder) {
            		tmpFunctions = ((Adder) func1).simplify().getFunctions();
            	} else {
            		tmpFunctions.add(func1);
            	}
            }
		}
	}
}
Multiplier類化簡核心程式碼(只包含主要邏輯)如下:
public Adder simplify() {
        for (Function func : functions
        ) {
            if (addFunctions.isEmpty()) {
                if (func instanceof Adder) {
                    tmpFunction = ((Adder) func).simplify();
                    addFunctions.addAll(tmpFunction.getFunctions());
                } else if (func instanceof Multiplier) {
                    tmpFunction = ((Multiplier) func).simplify();
                    addFunctions.addAll(tmpFunction.getFunctions());
                } else {
                    addFunctions.add(func);
                }
            } else {
                if (func instanceof Adder) {
                    tmpFunction = ((Adder) func).simplify();
                    multiFunctions.addAll(tmpFunction.getFunctions());
                } else if (func instanceof Multiplier) {
                    tmpFunction = ((Multiplier) func).simplify();
                    multiFunctions.addAll(tmpFunction.getFunctions());
                } else {
                    multiFunctions.add(func);
                }
                addFunctions = multi(addFunctions, multiFunctions);
            }
        }
        return new Adder(addFunctions);
 }

合併

​ 為了能夠更好地合併同類項,我單獨建了一個Standard類(成員變數如圖),可以表示隨後化簡表示式的每一個項,即形式為a\*x\*\*b\*[sin(inner)^c]*\*[cos(x)**d]*

​ 然後在Adder類新建一個HashMap,鍵值key為Standard類的物件,值value為該Standard的係數,也就是上式中的a,然後遍歷Adder下的functions(未化簡的項,可以想象每個function預設由加號連線組成表示式),對其包含的每個項進行合併,這裡的合併體現在程式碼上就是對比HashMap的鍵值,如果不存在,則直接存入;如果存在,則更新該項的value(即更新該項的係數)

​ 其中注意,對於HashMap中的鍵值,為了保證能夠真正意義合併同類項,需要重寫HashCode,我們只需要保證power,sins和coss一致,就能確定是統一形式的項

​ 還有一點需要注意,這裡重寫HashCode,對於其中的sins,coss由於含有inner,即三角函式中的因子,所以對於每一個function的子類都需要重寫一下HashCode來保證真正意義合併同類項

對於我所說的真正意義合併同類項,舉個例子,比如sin((x+x))和sin((2*x)),由於表現形式不太一樣,如果不重寫HashCode會導致這兩個生成的HashCode不一樣,從而無法合併

迭代開發

在以上重構了的基礎上,已經能夠很好地滿足第一次的要求,並且能夠很方便地進行第二次作業的迭代開發。於是,針對第二次新加的自定義函式和求和函式,基本上只用解決表示式解析部分的改動,後面的化簡與合併其實並不會影響到,這也體現了目前這一版本的架構的可擴充套件性

這一次作業作業新增了sin和cos以及自定義函式和求和函式,對於解析部分,新增sin和cos只需要在解析時識別到sin或cos的token以後,呼叫parseFactor對內部進行解析即可(這個已經能夠支撐第三次作業對sin和cos的inner解析了);而新增自定義函式和求和函式在這次作業中我選擇了在字串中進行暴力替換(雖然知道很不好,但當時還是這麼做了)

對於自定義函式,當字串解析時識別到f|g|h,把自定義函式呼叫這一部分提取出來,然後利用正則的捕獲組提取出函式呼叫部分,再把對應部分進行替換,這裡替換需要注意的是替換的順序,如果自變數中含有x,需要先把x的部分替換以後再進行後續替換,否則如果遇到f(y,x)=y-x時如果按照自變量出現的順序進行替換,就會出現y-x——>x-x,顯然這樣是錯誤的

對於求和函式,由於是sum(i,begin,end,..)的固定模式,直接把後面求和表示式中出現的i用begin逐漸+1知道begin進行替換。這個地方有四個坑點

  • 第一個是在替換i的時候,由於求和表示式中肯能出現正弦函式sin,所以這裡的i也會被替換掉,導致字串不能正常解析(這也證實了進行字串暴力替換的弊端)
  • 第二個是begin,end是常量因子,所以我們需要把記錄遍歷到的序號時用BigInteger來儲存,否則會資料溢位
  • 第三個是要特判begin>end情況,輸出為0
  • 第四個是,如果直接字串替換i,需要替換時在i兩邊加上括號,這個是我第三次強測以後才發現的問題,否則如果替換的數是負數時,會導致負號的作用範圍擴大到整個項

Task3架構

由於此次作業僅在第二次作業的基礎上做了少量的修改,所以這裡只對修改部分進行描述

由於這一次作業自定義函式的實參可以是求和函式、表示式因子也可以是自定義函式,函式可以互相呼叫,所以如果還是使用第二次作業的暴力替換法的話就不太能醒得通了,而且容易引入很多奇怪的bug,於是我對錶達式解析部分進行了修改,不在使用暴力替換,而是同樣採用遞迴下降法來解析,當識別到f|g|h或者sum以後,對其函式呼叫或求和表示式對parseExpression進行再次呼叫,這樣就只能保證表示式能夠解析到最簡形式然後不斷返回

基於度量分析

前置知識

  • ev(G) 基本複雜度是用來衡量程式非結構化程度的,非結構成分降低了程式的質量,增加了程式碼的維護難度,使程式難於理解。因此,基本複雜度高意味著非結構化程度高,難以模組化和維護。實際上,消除了一個錯誤有時會引起其他的錯誤。

  • iv(G) 模組設計複雜度是用來衡量模組判定結構,即模組和其他模組的呼叫關係。軟體模組設計複雜度高意味模組耦合度高,這將導致模組難於隔離、維護和複用。模組設計複雜度是從模組流程圖中移去那些不包含呼叫子模組的判定和迴圈結構後得出的圈複雜度,因此模組設計複雜度不能大於圈複雜度,通常是遠小於圈複雜度。

  • v(G) 是用來衡量一個模組判定結構的複雜程度,數量上表現為獨立路徑的條數,即合理的預防錯誤所需測試的最少路徑條數,圈複雜度大說明程式程式碼可能質量低且難於測試和維護,經驗表明,程式的可能錯誤和高的圈複雜度有著很大關係。

TASK1

程式碼規模分析

相比之下,用於表示式解析的Parser類和Lexer類程式碼量較多,顯得較為臃腫,而其他關於表示式架構儲存的類規模較為均衡

方法複雜度分析

縱觀所有方法,也就是在parser和lexer中出現了爆紅,這從上面的程式碼規模中這兩個類較為臃腫也就可以預料到

類複雜度分析

TASK2與TASK3

由於第三次作業在第二次作業上迭代開發,且僅進行了表示式解析部分的少量調整,所以直接對第三次作業進行度量分析

程式碼規模分析

方法複雜度分析

類複雜度分析

分析自己程式的bug

TASK1

這一次被強測和互測以後發現了一個bug,死於優化,經一次提交修復所有

輸入:(+-x*0*-3),輸出:00*x

看起來問題很大的樣子,其實就是因為遇到係數是0的項沒有用加號分隔

TASK2

這一次強測錯了一個點,互測沒有被hack出bug,經一次提交修復所有

輸入:

2
f(x,y,z)= (x)**+000-(cos(z)**2)**2
g(x)= +0-1009
(f(-0,sin(x),+0))**+02-sin(x**2)**+1 +-2-sum(i,1,10,(i*x))

輸出

-1+-55*x+-2*cos(0)**4+1*cos(0)**8+-1*sin(x*x)

原因是對於sin內部因子的列印,我選擇呼叫該因子的toSting(),而由於為了優化,將冪函式x**2簡化為x*x,而本次作業對輸出的要求是三角函式中只能包含冪函式或常量,所以輸出錯誤

TASK3

這一次強測和互測出來歸因於兩個bug,但是在5行內修復了這兩個bug,經過這三次懊悔於提交前沒有好好測試

  • 第一個:

    • 輸入:
    0
    -+0 +-x*sin(cos((1-1)))**+2+sin((-sum(i,1,1,i)))
    
    • 輸出:
    sin((-1))+-x*sin(cos(()))**2
    
    • 由於為了優化,對於Adder類的輸出,當判斷某一個標準項的係數為0時,直接return不輸出,繼續輸出下一個標準項,忘記了
  • 第二個:

    • 輸入:
    0
    sum(i,-1,1,i**2)
    
    • 輸出:空
    • 一方面由於本來就錯誤的輸出為0,導致輸出為空。另一方面,在對sum進行解析的時候,沒有考慮到負數代入的情況,沒有加括號,導致最後結果為-1**2+1**2=0

分析別人程式的bug

TASK1

/*第一個*/
輸入:
-(0)+1
輸出:0+-1*x**0
原因:符號的處理部分不對,負號作用到了後面的1
/*第二個*/
輸入:1
輸出:報錯
原因:該同學選擇了預解析模式,對於只有一個常量輸入時,沒有正確獲取資訊
/*第三個*/
輸入:+-(-x-+-2)**1*x+(+-x*0*-3)*(-+x**1*0+x**+1)**3*+2
輸出:-2*x+x**2+
原因:對於加號連線的時候最後一項為0時省略了0但是仍有加號輸出

TASK2

輸入:
0
sum(i,9999999999999999,10000000000000000,i*x)
輸出:報錯
原因:對於sum

TASK3

/*第一個*/
輸入:
0
x*(sum(i,2,1,(sin(x)+1)))
輸出:報錯
原因:不能解析begin>end的情況
/*第二個*/
輸入:
0
cos(0)
輸出:0
原因:為了優化把cos(0)直接誤判為輸出0
/*第三個*/
輸入:
0
sum(i,9999999999,10000000000,1)
輸出:報錯
原因:用Integer來存begin,end造成資料溢位

架構設計體驗

​ 首先,很感謝課程組第一次訓練給出的遞迴下降的框架,正是對這一框架的擴充讓整個程式在解析字串方面已經有很高的的魯棒性。

​ 其次,從第一次到第二次作業為了後續更好的迭代進行了重構,事實證明重構並不是什麼壞事,當發現原有框架不能滿足現有需求時果斷選擇重構才能將損失降到最小,否則後期在原有框架上不斷調整,終有一個時刻會坍塌。

​ 最後,由於我最後強測和互測發現的bug都是由於自己沒有在提交前對輸出部分進行嚴謹的審查,導致優化後出現了一些簡單到不能再簡單的bug。所以三次作業的bug修復我幾乎都是在互測時候hack別人的時候發現了自己的低階錯誤,並都在修復開放後5行之內解決掉bug。雖然修復bug很快速,但是這些低階錯誤真的應該在中測結束前就自己解決掉。

​ 下週就到多執行緒了,希望能夠在繁忙的四月也能很好地完成OO的課程任務。

​ 最後,如果本部落格出現筆誤或錯誤,歡迎批評指正!