面向對象第四單元做業/最終總結

1、本單元兩次做業的架構設計  

1.1 第一次做業架構設計

第一次做業的主要任務是基於類圖的一些查詢指令,大致來看架構的實現有兩種方式:java

一種是把類圖的結構反映到代碼的結構中,也就是爲類設置相應的數據結構,每一個類是一個實體,其餘的類圖關係能夠經過爲這個類添加相應的屬性實現。python

第二種方法是直接面向功能,建立類圖類,而後統一在內部設計各UML元素的數據結構。git

兩種方法的思路不同,第一種我稱之爲面向內容,由於程序的數據結構和類圖的內容息息相關,第二種我稱之爲面向功能,由於全部的數據結構全都是爲了實現某查詢指令而設置的。github

我選擇的方法是第二種,看起來,第一種思路是一個更天然的方法,由於你的程序的類設計能夠直接按照UML來,可是爲了方便功能的實現,我才用了第二種方法。正則表達式

架構設計具體細節以下:算法

Main:主控類,啓動程序。數據結構

UmlInteraction:交互類,負責信息收發。多線程

ClassGraph:類圖類,實現本次做業的核心要求。架構

Memo:記憶類,存儲以前查詢過的指令結果,加速查找。併發

1.2 第二次做業架構設計

基於第一次做業的架構直接拓展,對每一個圖,新建一個對應的類,而後分別實現,同時擴大UMLinteraction類。各種的功能以下:

Main:主控類,啓動程序。

UmlGeneralInteraction:交互類,負責信息收發。

ClassGraph:類圖類,實現上次做業的核心要求。

InteractionGraph:交互圖類,實現交互圖指令的要求。

StateMachineGraph:狀態圖類,實現狀態圖的指令要求。

Memo:記憶類,存儲以前查詢過的指令結果,加速查找。

Checker:規則檢查類,檢查各個規則。

這裏面又個問題,就是規則的檢查其實只是檢查了類圖,因此更天然地應該把這個檢查的方法放在類圖類裏,可是這樣一個類行數太多,雖然沒有超過代碼風格檢查的要求,可是分開放會好一點,因此我分紅了兩個類,這樣Checker爲了檢查就必需要傳入參數,就須要把類圖的有關信息傳入到Checker類裏,這樣的傳輸必需要保證傳入的量是不可變的,不然會致使檢查完規則以後類圖的含義發生變化,致使出錯。

雖然面向功能的架構設計未必是最簡潔的,也未必是最優雅的,可是這是一種思惟的訓練,這樣的設計證實我對於對象的理解已經超過了實體層次,上升到了邏輯抽象的層次,也就是說我設計對象再也不是直接把研究問題內容中的對象直接抽象出來,而是對功能進行抽象。

雖然這一單元做業由於沒有處理接口重名不重ID的問題不慎炸了一個點,可是實踐基本上證實這樣的設計仍是沒有什麼問題的。炸點更可能是由於我比較懶惰,沒有常常看討論區,也不太仔細看通知,尤爲是不少時候已經寫完了又出一些補充說明,這種狀況下就心累不想改了。

2、本身在四個單元中架構設計及OO方法理解的演進  

2.1 第一單元

本單元做業一共分爲三次,主要任務爲多項式求導,第一次僅要求對非複合的冪函數多項式求導,第二次增長了正弦餘弦的函數形式依然會出現複合,第三次則容許函數複合。要求程序在任何輸入狀況下都不會崩潰,且能正確識別出用戶輸入是否合法,對合法的輸入輸出儘可能短的求導結果,對不合法的輸入輸出WRONG FORMAT。

      首先整個程序的運行須要主控類,負責實現輸入、輸出、求導的過程控制。主控類可以把用戶輸入的字符串轉化爲內部的存儲結構,而這個存儲結構是由類實現的。通過對錶達式的分析,以及編譯技術學到的文法知識,不難發現,表達式由項構成。

在第一次做業中,項的組成成分單一,因此表達式和項足以應付全部狀況,而且輸入輸出能夠直接在表達式類中一次性完成。

      第二次做業雖然加入了新的函數類,可是考慮到他們對求導運算封閉,因此其實每一個項能夠寫成一個a*x^b*sin(x)^c*cos(x)^d的形式,所以我依然沒有建立因子類,我只須要對每一個項維護好abcd四個係數便可。

      第三次做業中,嵌套的出現,每一個項完全失去了統一性,因此必需要增長因子類,而因子有不少種,按照其種類,能夠分爲三個子類:冪函數子類、正弦函數子類和餘弦函數子類。隨着狀況的複雜,對於對象的構造和輸入的處理也不能一次完成,因此把對於輸出輸出的分析分發到各個類的構造函數中,求導過程也要如此。並且爲了不遞歸降低子程序分析的麻煩,我對字符串進行特殊處理,維持了正則表達式的使用。

綜上,我一共有主控類、多項式類、項類、因子類,而因子類做爲父類有冪函數子類、正弦函數子類和餘弦函數子類。

2.2 第二單元

三次做業的多線程設計出現了巨大的變化。

2.2.1 第一次做業:兩線程策略

  因爲第一次做業只有一部電梯,並且能夠採用先來先服務的傻瓜調度,按照封裝的思想,電梯內部執行任務的細節無需關心的話,和生產者消費者問題別無二致。因而採用了Collector線程讀取輸入,即生產者,而電梯線程執行請求,爲消費者。兩個線程之間須要互斥訪問請求隊列,因此另外抽象出Schedule類,把隊列及其方法封裝起來。這樣的設計中,調度器並非一個線程,電梯和Collector分別調用Scheduler的方法互斥訪問請求隊列,根據隊列是否爲空使用wait和notifyAll也能夠很容易地避免輪詢和死鎖。

2.2.2 第二次做業:三線程策略

  第二次做業雖然仍是一個電梯,可是增長了調度請求,因此調度器須要發揮做用。這裏存在一個對調度器的理解問題。調度器既能夠看做是一個協調區,只對各個請求進行排序,讓電梯自行對選擇的任務進行路徑規劃(捎帶);或者,電梯是一個只可以執行運動裝卸和開關門的機器,由調度器對輸入的請求(Request)進行翻譯轉化,轉化成簡單的指令(Order)。

  因爲對將來需求的誤判,我把程序向着有助於調度算法擴展的方向進行構造,因此我在第二次做業中加入了Scheduler線程,讓它對與傳來的請求進行翻譯,翻譯成一系列上樓和下樓的Order,若是有新來的請求能夠捎帶,就會在這個Order序列上插入上人和下人的Order,也就是說,電梯全部的動做,都有Schduler安排穩當。

  這樣的設計須要維護兩個隊列,一個隊列是Scheduler和Collector互斥訪問的Person request隊列,一個是Scheduler和電梯互斥訪問的Order隊列,Scheduler封裝全部的調度算法,也集中管理類全部的互斥訪問的方法。

  這樣的設計在當時的視角下,所具備的優勢是可以比較容易的擴展到多電梯的狀況,屆時只要再增長一個平衡調度的算法決定請求分發給那個電梯就好了。

  缺點在於,爲了防止電梯拿到一個過於巨大的請求而喪失了捎帶的機會:好比直接拿到從1樓到15樓的請求,結果這個過程當中電梯一直在休眠,那麼就不能相應中間能夠捎帶的請求。爲此,必須把每上一層樓看成一個Order,避免出現上述狀況,可是這樣會有不少Corner Case很是惱人,好比在電梯折返樓層由於方向不明確而出現一些不合常理的捎帶。

2.2.3 第三次做業:兩類線程的迴歸

  事實證實,第三次做業給出的全新需求徹底不能和我以前的設計兼容,由於電梯具備個性(不一樣的容量、速度、停靠樓層等)使得若是把調度問題徹底集中在調度器中會致使調度器過於複雜。因此,Scheduler應該僅僅被視爲是一個請求的收發裝置,協調各個電梯之間的任務分配。另外考慮到換乘,Scheduler也是各電梯進行同步的場所

  根據上面的認識,我又從新回到了第一次的設計,Scheduler再也不是一個線程,而是集中了各類共享隊列和方法的集合。

  這一次的共享對象有4個,三個電梯各自的字典,這是一個樓層到各層上下人的映射。以及一個等待隊列,用於電梯之間的換乘合做。Scheduler中針對這些數據結構進行操做,被電梯和Collector調用。

  Collector先從控制檯獲得請求,而後調用Scheduler的方法將其分發到電梯的字典中(映射了每一個樓層和該樓層上下的乘客),若是須要換乘的話,也須要將其加入到等待隊列中。電梯也互斥訪問字典取得請求。由於字典的存在,電梯每到一層就檢查一下是否有人上下電梯,這樣能夠避免Corner Case,並且能夠實現捎帶。同時,爲了兼顧效率與性能,個人調度算法具備內生隨機性,陷入極差狀況的機率極低。

  此外,我還專門設計了Scheme類,用來屏蔽個類電梯的停靠樓層等方面的差別,下降調配和分發的邏輯複雜度。任何請求都會先轉化成一個Scheme,裏面包含了請求的基本信息和換乘的要求(如前文所述,隨機化選擇),返回給Scheduler一個保證合法的分配方案,以後Scheduler在根據Scheme的安排拆分紅Order(這裏的Order和第二次做業含義不一樣,只是具備換乘信息的PersonRequest而已)加入到電梯中。

 

2.3 第三單元

  三次做業的總體架構幾乎不變。除了第三次增長了兩個類,其餘的幾乎不變。固然增長的兩個類主要是服務RailwaySystem,因此耦合度並不高。

      Path類。三次演化中,Path類是幾乎不變的,除了第二次意識到第一次每次都查一遍點致使超時因此增長了一個變量記錄DistinctNode外,就沒有更改過了。

      PathContainer/Graph/RailwaySystem類,這個類實現的方法在不斷增長,從PathContainer到Graph,這兩次做業之間的改動是不多的,只須要增長一些方法。以前實現的方法沒有任何改動。

      可是從Graph到RailwaySystem架構發生了比較大的變化。新增了兩個類,其中Pair類比較簡單,其實使用javafx.util.Pair便可,可是考慮到jar包運行的問題,我本身實現了一個簡單的Pair。另一個類MsGraph,這是一個圖類。這個類的主要做用是實現全部圖相關的數據結構和算法,由於第三次做業中有不少不一樣類型的圖。因此,此處的MsGraph的做用是純粹的圖類,所謂純粹,是由於其中任何的數據結構都不和本次問題發生關係,全部的節點和邊都是抽象的。而RailwaySystem類則主要實現用戶輸入到這張純粹圖的映射,爲圖類屏蔽問題的差別。因此,其實第三次做業中,MsGraph纔是Graph的演化,(BTW,Ms是My super的意思),MsGraph是在第二次實現的圖類基礎上增長了迪傑斯特拉方法求最短路產生的。而RailwaySystem則主要是創建各類索引讓各個圖相互配合,完成功能。

2.4 第四單元

見第一章。

2.5 理解的演進

對面向對象的理解不斷加深。

第一單元做業中,類的設計依賴於實際的數學公式,加上以前編譯技術對與語法的認識因此應該說沒有經歷不少思考就直接設計了一個大體方向,獲得了類的設計。

第二單元做業中,由於設計多線程問題,這方面第一次接觸,因此仔細研究了課上測試的代碼,學習了課上代碼對於多線程的設計理念,照貓畫虎是以學習爲主,本身的思考並不算太多,可是由於設計過程當中出現了一次線程個數的切換,因此對整個多線程架構的設計有了比較深刻的理解。

第三單元做業雖然聯繫的重點是按照規格寫代碼,可是針對這個問題的架構設計自己仍是值得思考。第一次做業已經明確規定了要求實現的接口,因此我就是用簡單至上的原則,直接一個接口一個類的實現了,很是簡單。第二次做業和第三次做業稍顯複雜,可是隨着對問題認識的深刻,我逐漸抽象出圖的概念,並把有關圖的基礎運算(最短路、搜索)等集中到一塊兒,並且我在構圖是儘量屏蔽了和圖自己不相關的信息,整個架構呈現三層,最外層用於表示,中間層用於轉化,最裏層是純粹的圖結構,和編碼方式所有無關。爲了屏蔽表示的複雜性引入中間層是計算機領域常見的方法。這樣的屏蔽雖然自己形成了必定的複雜性和開銷,可是卻可以爲程序的拓展提供便利,由於能夠把全部的改動限制在中間層及以上。我這樣的設計也確實爲第三次做業的完成提供了巨大的方便。

第四單元做業則是更加抽象的類設計。首先我明確區分了兩個概念,咱們研究的內容和咱們的設計。由於他們在這一單元做業中是重合的。UML是用來研究架構的,咱們的設計架構剛好就是UML的內容。這樣的重合性也許是一種干擾。很容易的咱們的設計思路就跟着圖自己走了。可是,考慮到UML工具StarUML的內部組織,老師上課也強調過,StarUML工具裏每一個元素各個字段的id是管理工具本身設計的,並不和所畫的類圖直接一一對應,因此我以爲這種區分是正確的思路,會給咱們的設計帶來必定的便利。

整體來看,對架構的設計從前兩次做業直接對研究的問題內容抽象,逐步演化到一種邏輯抽象,也就是爲了方便問題的解決抽象問題的解決過程,而後設計類去實現。

3、本身在四個單元中測試理解與實踐的演進

3.1 第一單元測試

  首先是手動測試,手動測試的時候,我加入了死循環,而且使用IDEA中Run with Coverage的模式運行,這個模式的好處是能夠在結束運行以後告訴你各段代碼時否被覆蓋,這種方法簡單快速,並且能讓你迅速有針對地把全部代碼執行一遍,甚至能夠起到簡化代碼的做用。我在第一次做業中使用這種方法,發現有幾處代碼不管如何也覆蓋不上,後來仔細分析了一下,是由於x不管如何不會成爲一個求導的結果,因此那裏的邏輯組合係數和指數都是1這個分支其實永遠不會進入,因此我果斷刪掉了這個分支。固然這個方法存在致命問題,爲了測試我不得不修改已經寫好的代碼,這樣測完了若是沒改回來,就可能形成致命風險,好比卡評測。

      以後是自動化測試,對於正確的用例,我寫了一個python腳本,能夠根據正則表達式,生成目標表達式,在使用python中subprocess指令,調用本身的java程序,識別控制檯輸出,而後使用python的sympy進行求導,計算。這個方法最大的好處是真正實現了黑盒測試,把須要測試的代碼使用子進程的方式啓動,利用管道獲取控制檯輸出。可是問題也很明顯,就是隨機生成的用例每每沒有針對性,並且由於正則表達式太複雜,稍微長一點的表達式,生成和計算都須要很長時間,並且還很容易超出計算限制。不過,這個方法至少實現了大量測試,在必定程度上確保程序的正確性。

     對於錯誤狀況分析,則比較難,理論上,只要正則表達式是正確的,看起來對於全部錯誤都會輸出WRONG FORMAT!。因此核心是創建正確的正則表達式分析方法,藉助編譯技術中學習的語法分析方法,從語法樹分析,按照DFA分析,最終獲得的正則表達式不會出問題。

3.2 第二單元測試

3.2.1 在設計上分析進行避免

  設計的時候,抽象出Scheduler類,把全部的互斥訪問工做所有集中在同一個類裏,方便設計和檢查,而且容易出問題的地方集中起來。

  此外,對於互斥場景的訪問,必定要使用規範的格式書寫代碼,力求一種對稱性和統一性,好比下面是我第二次做業中取出請求的代碼,嚴格按照同步——檢查(可選)——操做——喚醒的格式來寫。

View Code

  此外對於多個鎖的狀況,必定要避免嵌套加鎖,防止出現死等,爲此,我採起了分別加鎖處理的狀況,可是若是在中間切換,就可能出現潛在的數據一致性問題。因此我對於可能出現這種問題的狀況,我採起了檢查A——操做B——操做A的方法,避免出現對A直接操做後線程切換,而後出錯的狀況。

  可是第三次做業中須要同步訪問控制變得更加複雜,複雜來源主要是換乘的電梯通訊和關機指令的複雜。

  電梯的通訊要求對等待隊列進行互斥訪問,這個的實現和Order隊列大同小異。

  關機指令則略顯複雜,須要避免死鎖和插入異常。爲了不死鎖,當Scheduler發出關機信號後須要喚醒全部線程。此外,爲了防止出現插入異常(一個電梯完成換乘乘客的第一階段任務,要從等待隊列取出後,但還未插入到下一個電梯的指令隊列中前,發生了線程切換,這個電梯發現沒有任務插入且等待隊列爲空,收到關機信號後就可能會關機,致使後續的請求沒有被處理),須要先插入到電梯中再將其從等待隊列移除。

3.2.2 大規模測試

  在設計上儘量避免了死鎖和數據競爭狀況的出現之後,開展大規模隨機測試,隨機生成請求輸入後,檢查輸出的操做是否合法,通過了上百次測試後,能夠基本保證程序的正確性。

3.3 第三單元測試

3.3.1 使用Junit進行單元測試

      不一樣的方法測試難度並不同,容易測試的方法通常有兩個特徵,一是返回值可能的數量少好比布爾類型的方法,一種是邏輯比較簡單沒有算法好比增刪路徑。這些比較容易測試的方法主要是isConnected, CONTAINS_*以及addPath之類,能夠用Junit進行單元測試,構造幾個用例就基本有信心保證正確性。如下是第二次做業圖類測試部分Junit測試代碼。

View Code

      測試結果以下:

 

3.3.2 Corner Case測試

      對於路徑中有諸多平行邊,起點和終點一致的查詢等等corner case進行測試。好比:

PATH_ADD 1 2 2

CONTAINS_EDGE 2 2

PATH_ADD 1 2 2 2 3

PATH_REMOVE 1 2 2

CONTAINS_EDGE 2 2

      諸如此類的corner case還有不少,通過測試後都沒有問題。可是這樣的測試極爲有限,並且並不能保證正確性。

3.3.3 搭建對拍框架進行多人對拍

      還有一些方法並不容易測試,好比最短距離等等,即便是寫一個對拍程序,對拍程序自己的正確性也不易保證。此外各個方法之間的綜合做用是否會出問題也不容易使用Junit測試。因此必須進行整合測試。可是此次做業不像電梯,電梯能夠有一個另外的邏輯推斷結果的合理性,可是此次測試並無。但是咱們又沒有標程,爲此只能經過羣體智慧進行測試。隨機生成測試用例後,運行若干同窗的jar包,而後把結果進行比對。若是你們輸出的結果都同樣,那麼就有比較大的把握認爲程序是正確的,若是有一我的和其餘人都不同,那大機率是這我的錯了。

      我搭建了一個多人對拍框架,它具備如下幾個特徵:

      1 併發測試:同時運行多組java進程進行測試提升測試效率。採用python線程池,每一個python線程開啓一個新的Java進程。

View Code

      2 計時服務:提供時間統計服務,做爲算法執行效率的參考。

      3 郵件通知:運行大規模測試很耗時,因此我是在樹莓派上跑的,隔一段時間check一下樹莓派很麻煩,因此我設置郵件通知方法,運行完之後向我發送郵件。

      當沒有發現不一樣時,部分結果以下:

 

      具體技術細節見開源代碼庫:https://github.com/sdycodes/JavaDestroyCorner.git  (開源已通過jar包擁有者贊成)

3.4 第四單元測試

3.4.1 面向查詢指令構造Corner Case

最歐一個單元的測試中,才用了先構造測試用例後寫代碼的方法。現根據須要完成的指令,考慮一些比較特殊的狀況,以及通常的狀況,構造測試用例。而後再進行代碼實現。下圖是幾個測試的例子。

3.4.2 大規模隨機測試

這一單元做業開展大規模測試是很困難的,主要是由於UML圖的繪製不能隨機生成,因此其實隨機生成的只能是一些指令,若是類圖自己不夠複雜,其實再多的指令也並無太大意義,這反過來又進一步說明了手動構造測試數據的重要性。

3.5 測試的理解與演進

我從一開始就重視測試的要求,從一第一單元做業開始,我就是用了大規模測試的方法來儘量避免程序錯誤,可是由於一思惟惰性對於cornercase不肯意去想,總以爲大規模暴力測試應該能夠實現絕大多數狀況的覆蓋。這樣雖然我測試了大量的樣例,可是其實測試是比較低效的,不過採用樹莓派24h不間斷運行倒也沒有太大問題。

第二單元做業的測試有難度,主要是由於多線程,而後模擬真實電梯的運行,速度很慢,可是檢查正確性的邏輯並不複雜,能夠按照每一個人的軌跡去檢查。

前兩次做業自動化測試都是比較簡單的,由於檢查正確性有另外的方法,好比表達式求導的正確性,只要帶入數值檢查,電梯的正確性,只要檢查每一個人的乘電梯的軌跡是否合理便可。可是第三次第四次做業,檢查起來就有難度了,由於沒有另外的邏輯,檢查須要的邏輯和求解問題同樣,好比求解最短路是用來Dijstra算法,那麼驗證的時候仍是須要算一個Dijstra,這樣若是兩個dij出自一人之手,其實檢查並沒有意義。因此,我搭建了多人對拍框架,相似對答案的方式,同窗之間相互認證,出現不同你們一塊兒探討,保證程序正確性。

前面三次做業中,我高度依賴自動化測試,緣由很簡單,測試用例的構造能夠所有隨機生成,因此與其處心積慮構造測試用例,不如直接自動生成測試來的簡單穩妥。每次測試都部署在樹莓派上7*24h不間斷運行,因此也不太須要擔憂有什麼特殊的測試點沒有測到一類的問題。可是第四次做業倒是遇到了自動化測試的困境,由於UML圖實在不能隨機生成,因此這又逼迫我從新迴歸了手動設計。

整體來看,前三個單元的做業讓我實現自動化測試的水平不斷提升,而第四單元的做業有讓我從新迴歸對問題自己的思考。此外,測試和實現的順序也在第四次做業發生轉換。

這個過程還伴隨着其餘工具的使用,好比JUnit,JProfiler以及IDEA自帶的插件等等,這些工具的使用也都可以方便我去測試,發現bug,特別是Coverage的評估可以指導我構造出高效的測試代碼。

4、課程收穫

4.1 工具鏈的使用

這門課接觸了不少工具。

IDE:IntelJ IDEA

單元測試工具:JUnit

線程工具:JProfiler

UML工具:StarUML、z3

瞭解了不少語言和表示方法:

Java語言、JML語言、UML圖的規範

4.2 工程代碼能力的提高

由於代碼風格的檢查,指引我造成良好的命名、縮進、加括號的習慣。這樣寫出的代碼纔有可能成爲有質量的代碼。

結合IDEA的自動補全、代碼建議、快捷鍵的使用,寫代碼的速度和效率極大的提高,靈活使用條件斷點、變量監視等方法極大地加快了debug速度,認識到一個高效的生產力工具是何等重要。

這也是第一次進行系統性的Java代碼書寫,Java是比較廣泛使用的語言,掌握之頗有必要。不過Java裏面的還有不少複雜的語法包括面向對象的特性我尚未用到,往後還要繼續學習。

多線程能力的訓練,第一次實戰多線程,在OS課上學過管程,當時就發現Java實現併發控制本質上就是管程,因此還算比較快的認識到這一點。多線程程序是比較有意思的,不過出了錯誤不能復現確實比較有挑戰。

4.3 面向對象思惟的訓練

這是這門課一個很重要的學習目標,也是我收穫最大的一部分。

從第一單元開始,這門課就不斷強調關於面向對象的設計理念,除了掌握了一些基本的說法和麪向對象的概念之外,在這四個單元的做業中反覆強化的架構設計纔是對面向對象理念進行學習的最好方法。而這種潛移默化的能力訓練很重要,可是卻有不易表達,可是從幾回做業的架構設計演進中能夠略知一二。

5、立足於本身的體會給課程提三個具體改進建議

5.1 關於性能分的意見。

比誰短、比誰快我以爲很差,助教也已經說過這個東西主要是給學有餘力的同窗作,那麼問題就來了,首先,學有餘力的同窗是否須要這點分數的激勵,其次,學有餘力的同窗想不想作這個事,我以爲這些都應該思考。有想積極探索的同窗,這個應該鼓勵,可是是否是能夠在其餘方面給予獎勵。

5.2 分數計算方法

聽說是按照排位給分,這個很殘忍,我知道大家會說競爭很重要,社會很殘酷,我也不反對,但在公開場合我想走人道主義路線。

5.3 關於課上實驗

高工每次實驗的時候都已經對所學知識很熟練了,沒有起到趁熱打鐵的做用。

相關文章
相關標籤/搜索