天天看點

面向對象第一單元訓練總結

一、前言

  "面向對象程式設計語言的問題在于,它總是附帶着所有它需要的隐含環境。你想要一個香蕉,但得到的卻是一個大猩猩拿着香蕉,而其還有整個叢林。"

— Joe Armstrong(Erlang語言發明人)

  在許多個夜晚,當我面對着Eclipse複雜的界面,仔細思索自己前幾天剛剛寫下來的、現在卻已經不知道是什麼意思的代碼的時候,我常常會質問自己:如果沒有了IDE,你還能寫得下去代碼嗎?令人遺憾的是,直到目前為止,無論我什麼時候問自己這個問題,得到的答案都是否定的——當我為了添加一個新的功能,向一個類增添一個屬性或方法的時候,連帶着就需要向其它類添加其它的屬性或方法,進而為這個類提供它所需要的資料;而為了提供這些資料,這些類又需要向另外的類申請更多的資料,直到所有的類混在一起,成為一灘稀泥,吓跑所有試圖去維護它的人。最終的結果就是,我自己也忘記了我的類裡面有哪些資料和方法,那些用于核心算法的方法和用于向其它類提供資料的方法互相交纏在一起,把整個類撐成了一個畸形的胖子。我隻好将自己的顯示器豎過來擺放,以便自己可以在一頁裡完整地看完一個類的定義——這還是在我的顯示器長寬比是21:9的前提下。然而即便如此,如果沒有了自動補全,我将依然寸步難行。

  而調試的過程則更加絕望。有兩個類,它們仿佛要比賽似的一個比一個長,卻你中有我我中有你,每修改一處微小的地方,就會引發一連串的連鎖反應,導緻Bug從莫名其妙的地方冒出來。而在試圖去修複這些剛冒出來的Bug時,又會引發新的Bug。不借助于IDE的單步調試功能,我甚至無法确定在自己的代碼中,Bug可能會出現在哪一個方法中——因為每一個方法都與另外的方法産生了強耦合,引發一個方法中Bug的代碼很可能在另一個方法中。即使我幸運地調好了一個Bug,我依然不敢肯定自己的程式能夠通過之前已經測試過的樣例,于是我隻好把之前的樣例重新再跑一遍,并祈禱它不要出錯。這樣一連串的過程下來,再多的時間也不夠用。

  本次總結,正是為了解決以上的兩個問題。

二、三次作業的程式結構分析

1、第一次作業 – 多項式加減

面向對象第一單元訓練總結
面向對象第一單元訓練總結

  設計回顧:為了避免正規表達式爆棧,解析多項式階段采用了雙層解析的方法,先解析外層花括号,在解析内層小括号,有效避免了正規表達式效率低的缺陷。在整體的設計上,解析子產品與計算子產品分離,計算子產品一旦獲得一個多項式,就一定獲得的是正确的多項式,減少了輸入和計算之間的耦合。計算時則采取一了一個相對簡單的算法,先對每個多項式按幂次升序排序,然後再依次歸并。

  自我評價:可能是由于之前在學習Python的時候有了一點點面向對象的思維基礎,第一次作業寫起來,我大體上遵循了面向對象的程式設計思路,而不是像C語言那樣把所有東西都堆在主函數裡。這次作業進行得比較順利,在公測和互測中都有着不錯的成績。

2、第二次作業 – 單電梯傻瓜排程

面向對象第一單元訓練總結
面向對象第一單元訓練總結

  設計回顧:沿襲第一次作業的設計,解析部分與計算部分合理地分離,為核心算法的實作減少了難度。由于采用了傻瓜排程,排程器類的代碼比較少,其核心方法為Schedule方法,每一次調用該方法,該方法從請求隊列中選擇下一個要執行的請求,并喂給電梯類去執行。電梯類承擔了主要的狀态儲存工作。

  自我評價:第二次作業的難度相比第一次作業有了一個難度上的提高,這主要是由算法帶來的。第一次作業可謂根本不需要算法,但對于電梯排程問題,即使是傻瓜排程,如何判斷同質請求也是一個算法上的難點。在這次作業中,我采用了一個比較簡潔的算法來判斷請求是否同質,時間複雜度比較低,代碼實作也很簡單。但萬萬沒想到,這個看似簡潔的算法為第三次作業挖了一個大坑。

3、第三次作業 – 單電梯ALS排程

面向對象第一單元訓練總結
面向對象第一單元訓練總結

  設計回顧:這一次作業相比第二次作業,重寫了排程器類的Schedul方法,并大幅調整了電梯類的代碼結構。Schedule方法依舊保持着其優秀的特性:每次調用,從請求隊列中傳回下一個将被執行請求。但為了保持這個特性,排程器類的代碼變的冗長,并且電梯類增加了許多事後看來多餘的方法,從整體上來說,這個設計相比第二次作業的設計,顯得異常繁雜而醜陋。

  個人評價:第三次作業的算法難度相比第二次作業又提高了一個層次。在這次作業中,想要又快又好地寫完,就必須有一個優秀的算法。然而我在寫這次作業的過程中,發現第二次作業的算法無法很簡單地移植到第三次作業上來,又限于繼承Scheduler類的要求不能全盤颠覆上一次的代碼,是以隻能在現有的基礎上不斷打更新檔來完成需求,造成了我在前言中遇到的那兩個問題。第三次作業總體上來說是失敗的,程式主算法的時間複雜度達到了平方級别,并且代碼又臭又長,難于調試和修改。如果有機會的話,重構應該是一個必選選項。

三、自己程式中存在的Bug

  這三次作業在測試環節總體來說還是比較幸運的,隻在第三次作業被别人挑出了一個涉及到100條輸入邊界判斷的小Bug,其它的地方都沒有被查出問題。當然,這個Bug是确實存在的,對此我也無可反駁。我分析了一下這個Bug出現的原因——我在對輸入的請求進行是否合法的判斷時,是分兩個步驟進行的,第一個步驟是正規表達式的過濾,第二個步驟是判斷特殊情況(一層向下或十層向上),其中任何一個步驟不通過都會立即報錯,但隻有第二個步驟才會檢驗是否超過100條指令。這意味着我的輸入判斷流程應該加以改變:把這兩個步驟合并成一個步驟,并進行統一的報錯和邊界判斷。

從程式結構上來講,将輸入的合法性判斷放在兩個類裡确實不是一個好的設計。在面向對象的程式設計中,一個類應該一次性做完它所應做的工作,并把正确的結果傳遞給其它的類。這個Bug歸根結底,還是設計上的欠缺和疏漏。

四、如何發現别人的Bug

  我不太願意去摳别人邊角上的細節,是以我的測試主要集中在功能性方面,有時可能會稍微測一點Readme相關的東西,但絕不會做出跟别人玩文字遊戲這種缺德  事。與其絞盡腦汁去惡心别人,不如用這時間去努力提高自己的程式設計水準。

  拿到測試任務後,首先把自己構造的、用于測自己程式的測試用例在别人的程式上跑一遍。如果前期構造測試用例上花了功夫,這個時候應該就已經能測出絕大多數的Bug了。如果沒有的話,再将程式下載下傳下來,并通過IDE的單步調試功能分析對方代碼的行為,尋找對方代碼中的邏輯漏洞,并針對性地根據這些漏洞設計測試樣例。最後,檢視Readme,對邊界條件和特殊情況進行檢查。這一套流程做完之後,如果找不到Bug,那就意味着基本沒有Bug了。

五、心得與體會

  在前言中我提到,本次總結的最終目标在于解決類之間耦合性過強導緻的代碼冗長、調試不易的問題。經過對之前三次作業代碼的度量和分析,我有了了以下這個即将支配我今後的作業的心得和體會——

  把算法從戰略需求變成戰術需求。

  這聽起來很玄,但說透了卻很簡單。在以往的C語言程式中,程式=算法+資料結構的鐵律支配着每一次作業的設計和實作,算法是整個程式的戰略需求。是以,C語言代碼的編寫過程中,一切都是為算法服務的——執行核心算法的函數可以從任何地方獲得它需要的任何資料,它的觸角伸到程式的每一個地方,像一個全知全能的神。這使得在C語言中構造一個大型的算法非常簡單,但也導緻了用C語言寫出來的程式稍一不注意就會有非常強的耦合性。

  在Java中,類的封裝特性使得在不同的方法(這裡特指不同類中的不同方法)之間傳遞參數變得比較困難。是以,如果再沿襲C語言的設計思路,把算法放在戰略的層面,一切資料和方法都為算法服務,勢必會導緻每一個類都互相糾纏在一起,而類屬性的Private可見性會使它變得更加複雜。可以說,在Java中,一個算法不适合橫跨多個類而存在,如果硬要把算法作為整個程式的戰略目标,讓一個類去承擔全部的核心算法,就不太可能避免類之間的強耦合。

  然而算法又是如此的重要,絕不可能忽視它的存在。對此,我的解決辦法是,把算法在整個程式中的重要性做一個降級——從戰略層面降到戰術層面,盡量確定一個算法隻需要類内部的資料就可以實作。這就需要在一開始對程式各個類做出一個良好而完整的規劃。在Java程式設計中,頂層規劃取代了原來的頂層算法,一個大的、戰略層面上的算法被拆成幾個小的、戰術層面上的算法,交給各自所屬的類去實作。通過類的封裝,把算法禁锢在一個小的範圍裡,不讓它去調用其它類中的資料。這樣,就可以很好地避免類之間資料的耦合,也就能最終解決我遇到的難題。

  總而言之,用Java編寫程式的時候,思路和之前的C語言可以說是完全不同的。C語言可以立刻開始寫起,不需要過多的構思,因為所有的資料都是唾手可得的,一個函數想要擷取資料不存在任何難度。但在Java裡,私有屬性大大增加了其它的類通路本類資料的難度,為了防止後期不斷增增補補,一開始就要對程式作出一個整體的構思和規劃。如果規劃合适,那寫出一手漂亮的代碼,不過是動動手指的事。