面向对象设计与构造第一次总结作业
第一次作業——多項式計算
---結構分析
第一次作業我只使用了兩個類,正像下面的類圖所表示的那樣,分別是Poly和ComputePoly。Poly類是不可變的,能保存一個多項式,可以進行加、減運算。ComputePoly是程序的主類,能夠讀取一個多項式加減運算表達式的字符串,并輸出計算結果。parseExpression方法通過調用parsePoly方法和parseOperator方法將輸入的字符串轉換為Poly對象和運算符列表。compute方法從polyList和opList取出多項式和運算符進行計算,返回計算結果。
總的代碼量是248行(后面的度量分析圖中可以看到),其中ComputePoly類占167行,Poly類81行;程序總共有28個方法,Poly占10個,而ComputePoly占18個。由于寫Poly類的時候參考了教材的寫法,Poly還算比較合適,無論是從兩個類的代碼量還是方法個數來看,ComputePoly都顯得不太協調。事實上也如此,ComputePoly做了除計算外所有的事情:處理輸入(包括錯誤處理,提取多項式),將多項式和運算符們存到數組里,調用compute方法得出結果,最后輸出結果。這是典型的面向過程式的思路,頗有一點用C語言寫程序的味道。
再仔細一想,ComputePoly做這么多事情合適嗎?就這個不那么復雜的程序而言,管理ComputePoly所做的事情還是能夠接受的,但是再實現一個乘法功能呢?很明顯,上面的兩個類都需要做修改,對于Poly而言,只需增加一個計算乘法的方法就可以了,但是對ComputePoly來說,第1,4,5,6共4個方法都需要修改。假如按照建議設計那樣將ComputePoly進一步分成3個類InputHandler,PolyManager,PolyArithmetic,同樣地,輸入處理InputHandler要做修改,PolyManager也需要修改,但PolyArithmetic不需要修改。實際該改的都得改,說白了還是前面所說的第1,4,5,6個方法。但是在這樣一種設計下進行修改的復雜性就降低了,修改InputHandler時只需要記得正則表達式改一下能匹配乘號,而不需關心乘法和加法在一起時的誰先計算誰后計算的問題;而修改PolyManager時也無需關注輸入是否處理好了,只用專心實現calculate方法和appendOperator方法(如下圖)。這樣一來不用在ComputePoly長長的代碼中苦苦尋找某個方法,上改下改,減少出錯的可能性,二來也不會被整體的復雜性所煩擾,分解成兩個問題后可單獨實現。
無論如何,在初識面向對象設計前我還是帶著面向過程式的思維,幸好能夠從教材上學習到如何編寫Poly類(非常感謝第一次作業時有這么好一個范例),初步領略了OO之美。下圖是用eclipse的metrics插件對第一次作業代碼分析得到的結果。重點關注一下第一個McCabe Cyclomatic Complexity,它中文名叫圈復雜度,是流程控制圖中獨立路徑的數目,主要由分支和循環個數決定,越大表明越復雜,測試時所需考慮的情況也就越多(因為每個路徑都要被測試),當它非常大的時候,程序的測試就變得十分復雜。這里的最大值是10,還能接受。仔細查看,造成最大值的方法是ComputePoly里的parsePoly方法,原因可能是需要進行輸入檢查,涉及的不同錯誤輸入種類數較多。
? Nested Block Depth是if,while,for的嵌套深度。這里是最大值3,處于正常范圍。最大值一個來自Poly的add方法,考慮到實現兩個多項式加法的細節,是可以接受的,另一個同樣來自parsePoly方法,原因同樣是涉及輸入錯誤處理。
再看一下Method Lines of Code,方法的代碼行數,這里最大值是26,不算太大。
---BUG分析
此次作業的bug在于正則表達式的匹配,我使用一個正則表達式試著去匹配整個輸入的字符串,潛在的危險是堆棧溢出。在分類樹上是有“壓力測試”這一項的,而且助教也說過注意不要爆棧,但是當時沒想明白什么會導致棧溢出,而且是第一次使用正則表達式,主觀上也認為壓力測試沒什么用,就沒有構造相應的測試樣例,導致了這個bug的產生。bug出現在isCorrectFormat方法中,這個方法檢查了輸入的字符串是否符合規定的格式。要解決這個問題可采用分段匹配的方式,整個字符串是多個多項式,中間用加減號連接,因此可以分別匹配每個多項式。
測試同學代碼時,我使用了每個分支樹結點的對應測試用例(除了壓力測試)。同學的代碼中存在類似以“f”,“ff”,“fff”命名的變量,我嘗試著去理解同學的意圖,再加上大量的分支循環嵌套,實在是難以揣測。
第二次作業
---結構分析
第二次作業一開始我花了一天的時間理解作業指導書,從頭到尾讀了好幾遍才弄清楚。到第三次作業也是如此,我試著邊讀邊用自己的語言去表達指導書的規定,并且舉出例子,然后分條把覺得重要的點寫在紙上,這樣做有些笨拙和繁瑣,不過的確能幫助我理解指導書的意圖。
課件中給出了提示,要構造電梯類、調度類、請求類、請求隊列類和樓層類共5個類(如下圖)。要怎樣確定該哪個類該做什么,這是除了理解指導書外另一件頭疼的事情。左思右想,苦思冥想,難以劃分各個類的職責。另一個問題是調度器類的command和schedule方法,弄得我一頭霧水,寫完后都沒能參透其中奧秘,直到看了互測同學活生生的代碼,才恍然大悟。
?
第二次作業的要點是如何把程序功能均衡地分配給各個類,如何讓多個類之間協同工作,要避免出現Idiot Class和God Class。從下面的類圖中可以看到Floor類就是比較白癡的一個類,它只知道樓層頂樓和底樓的編號。其實,可以考慮讓樓層類知道更多的信息,比如某層樓是否有電梯到達。
除了出現了一個Idiot Class外,另一個缺點是,在main方法里展開對輸入的處理,這與第一次作業相同,由于自己沒能意識到這種做法的壞處,在第二、三次作業時仍沒有加以改正。
本次作業的設計是否均衡呢?下面就再用定量的方法分析一下。?
?
每個類的屬性個數、方法個數、代碼行數如下面的表格所示,其中方法個數包括了構造方法。從數據上看,方差較大。代碼行數最多的類是ElevatorSystem,可能是因為在這個類里做了輸入處理,如果把輸入的處理分開來,應該會更均衡一些。
| 類 | ?Elevator | ElevatorSystem? | Floor | Request? | RequestQuue? | Scheduler? | 均值? | 方差? |
| 屬性個數 | ?7 | 4? | 2? | 4 | 2? | 1? | ?3.3 | ?4.6 |
| 方法個數 | ?14 | ?2 | ?4 | ?9 | ?10 | ?6 | ?7.5 | ?19.1 |
| 類代碼行數 | ?95 | ?107 | ?18 | ?48 | ?38 | ?55 | ?60.2 | 1170.2? |
一個比較大的數據是電梯類的方法個數14,其中用于狀態查詢的方法占了一半。由于事先未規劃好,在編碼的時候為了方便,新增了一些方法。仔細分析會發現一些方法是冗余的,比如getStatus方法,事實上這個方法也從未被調用過。另一個原因可能是題目要求的電梯狀態是定義在左開右閉區間上的,有時候為了方便我會使用左閉右開區間,這也增加了一定的復雜性。
再看一下類的職責是否明確。
拿電梯類舉例,它總共有14個方法,方法總數占到整個程序約1/3,但仔細看,只發現能夠讓電梯改變狀態的只有前兩個方法readyToGotoFloor和run,run方法是讓電梯運行到0.5s后的狀態,而前者確定電梯的下一個目標。也就是說,別的類只能告訴電梯下一次去哪個樓層,電梯只管去,并且自己決定方向,其他類不能干涉電梯的運動方向。假設其他類能直接修改電梯的方向,那么在這個設計中,如果調度器讓電梯向下走,但又是去往樓層數高的地方,這明顯是不合適的。電梯內部不存在請求隊列,無論何時,電梯都只有一個目標,它不用操心有多少請求在隊列中等著它執行,只用聽從調度類的指揮就可以了。
從功能的角度上看,電梯的職責是明確而單一的。但是這樣做的復雜性在于,電梯調度類需要精心地設計,在每次給電梯發送命令前,需要使用電梯類提供的一系列狀態查詢方法檢查電梯狀態(之所以要檢查是因為調用readyToGoToFloor會立即改變電梯的運動狀態,例如當電梯向上運行時,調用方法讓電梯去往比當前樓層數小的樓層,運動方向就會突然改變)。因此,調度類必須充分了解電梯各個狀態的含義(盡管它不需要了解電梯是怎樣確定自己的狀態的)和一些內部細節,否則就可能會導致電梯出故障。這就在一定程度上增加了電梯類與調度類的耦合性,一是使編碼時復雜性增加,二來修改、新增功能時容易出錯(例如我在寫第三次作業的時候就在這上面犯了很多錯誤)。
? 下圖是第二次作業的度量分析結果。可以明顯地看到標紅的圈復雜度較高,最大值是17。進一步細看(圖中未給出)可以發現高復雜度的來源主要是ElevatorSystem類的parseRequest方法和main方法,以及Elevator類的run方法。前者是由于輸入的錯誤情況較多,個人寫得比較凌亂,判斷邏輯復雜,main方法里也做了輸入的處理。后者是電梯運行時的邏輯稍微復雜,分支較多,也有兩層嵌套的情況。
再看一下每個方法的行數(圖中最后一行),最大值是46,與第一次作業相比有所增加,其來源同樣是parseRequest和main方法。如果將輸入處理部分單獨封裝在一個類中,并且優化一下錯誤處理的邏輯,應該能使整體設計均衡一些。
---BUG分析
這次作業的bug是在時間很大時運行的時間較長,需要十幾秒,這是由于實現采用了每0.5s進行一次操作的方式。
第三次作業
---結構分析
本次作業在前一次作業的基礎上增加了捎帶功能,用繼承的方式實現了ALSScheduler,對其他的類也做了一些調整。
?
由于保留了第二次作業的大部分內容,本次作業在均衡性上沒有改進,反而由于增加捎帶功能后變差。尤其是ALSScheduler,代碼行數最多,邏輯也較復雜。
| 類 | ?Elevator | ElevatorSystem? | Floor | Request? | RequestQuue? | Scheduler? | ALSScheduler | 均值? | 方差? |
| 屬性個數 | ?8 | 4? | 2? | 4 | 2? | 3 | 4 | ?3.9 | ?4.1 |
| 方法個數 | ?17 | ?2 | ?4 | ?12 | ?11 | ?5 | 12 | ?9 | ?29.3 |
| 類代碼行數 | ?108 | ?108 | ?18 | ?59 | ?48 | ?55 | 141 | ?76.7 | 1857.9 |
細心的讀者可能會發現這次的圈復雜度下降了1,但這不是因為進行了優化,只是做了點微調。這里最大值16也不是前面提到的輸入處理帶來的,而是來自ALSScheduler的command方法。為了實現捎帶功能,我增加了很多條件判斷,既難寫,又難以理解和修改。
---BUG分析
上面的3點都是給我互測的同學發現的,這里要感謝這位同學。
最后再分析一下第2、3個bug與設計結構的關系。這兩個bug都位于ALSScheduler類中,具體在多個方法中都有體現,究其原因,是使用了Java標準類庫中的優先隊列。這個隊列專用于捎帶隊列,我按照到達樓層的時間(先后)作為各個請求的優先級,能夠最早達到的,排在隊首,晚到的,排在后面。問題出在同一時刻進隊的請求,在出隊時可能失去了輸入時的順序以及請求發出的時間順序,這就導致了第2個bug的產生。另一方面,當主請求執行結束時,處在捎帶隊列隊首的請求未必是按照請求發出時間最早的。
我能想到的解決辦法是專門實現捎帶請求隊列類,兼顧到達時間順序與請求時間順序。在下一次作業中我會嘗試著改正。
?
轉載于:https://www.cnblogs.com/eggert/p/8688046.html
總結
以上是生活随笔為你收集整理的面向对象设计与构造第一次总结作业的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: liunx之Centos6.8杀毒软件的
- 下一篇: python字符串常见操作