BUAA OO 2019 第一單元作業總結

来源:https://www.cnblogs.com/lutingwang/archive/2019/03/26/BUAA_OO_2019_1.html
-Advertisement-
Play Games

[TOC] 總 本博文是2019年北航面向對象(OO)課程第一單元作業(多項式求導)的總結。三次作業的要求大致如下: 第一次作業:簡單冪函數的求導,如 $1 + x^5 + 4 x^3$ 第二次作業:簡單冪函數和簡單正餘弦函數的求導,如 $ 5 sin(x)^2+5 cos(x) cos(x)+12 ...


目錄

本博文是2019年北航面向對象(OO)課程第一單元作業(多項式求導)的總結。三次作業的要求大致如下:

  • 第一次作業:簡單冪函數的求導,如 \(1 + x^5 + 4 * x^3\)
  • 第二次作業:簡單冪函數和簡單正餘弦函數的求導,如 \(-5*sin(x)^2+5*cos(x)*cos(x)+12*x^2\)
  • 第三次作業:簡單冪函數和有嵌套的正餘弦函數的求導,如 \(2*(cos(x) + 1)*cos((2*x)) - sin(x)*sin((2*x))\)

源代碼及項目要求均已發佈到 github ,讀者可以下載檢查。以下將對這一單元作業進行簡單總結。

架構

項目的總體構架參考 \(MVC\) 模式,將運算與輸入輸出分離。由於輸入輸出都是在 \(console\) 完成的,因此並沒有 \(view\) 類,但 \(Model\)\(Controller\) 都有其對應類。

Controller​

輸入處理類 \(PolyBuild\) ,負責將輸入字元串組織成一個 \(Poly\) 對象。其中按層次包含五個解析方法:

  • \(parsePoly\)
  • \(parseItem\)
  • \(parseFactor\)
  • \(parseElement\)
  • \(parseTri\)

方法的功能顯而易見,採取遞歸下降法應用有限狀態機對字元串進行解析。

Model​

\(Model\) 對應 \(Package\ Poly\) 。如下 \(UML\) 圖所示,我將多項式分為四個層次:

  • \(Element\) :底層類,構成多項式的最基本元素,也是因數的底數。包括 \(Const\), \(Var\), 和 \(Tri\)
  • \(Factor\) :因數類,構成項的單元,屬於指數函數。由一個 \(Element\) 和其對應的 \(exp\) 對應。
  • \(Item\) :項類,構成多項式的單元,由多個 \(Factor\) 或表達式因數相乘組成。
  • \(Poly\) :頂層類,由多個 \(Item\) 相加構成。

每個類實現各自的求導方法和輸出方法( \(toString()\) ) 。

輸入處理

處理輸入字元串時,我採用了遞歸下降法,按層用狀態機進行處理。狀態機如圖:

Finite State Machine Illustration

上圖為示意圖,圖中狀態可能附加處理操作,並沒有列出,讀者可以下載代碼查看。虛線表示狀態轉移時,輸入字元串會被改變。線上字元表示狀態轉移規則,即當前游標下的字元,但並不是每次狀態轉移游標都會移至下一位。\(\phi\) 表示游標到達字元串結尾。

代碼靜態分析

以下使用 \(Metrics\)\(Statistics\) 插件對最終項目代碼進行靜態分析。

行數

Source File Total Lines Source Code Lines Source Code Lines[%] Comment Lines Comment Lines[%] Blank Lines Blank Lines[%]
Const.java 88 68 0.77 4 0.04 16 0.18
Derivable.java 20 14 0.70 0 0.0 6 0.30
Element.java 60 44 0.73 4 0.06 12 0.20
Factor.java 146 113 0.77 11 0.07 22 0.15
Item.java 294 255 0.86 8 0.02 31 0.10
Poly.java 189 158 0.83 8 0.04 23 0.12
PolyBuild.java 348 307 0.88 11 0.03 30 0.08
PolyBuildTest.java 30 24 0.80 0 0.0 6 0.20
Se.java 7 7 1.00 0 0.0 0 0.0
Tri.java 71 56 0.78 7 0.09 8 0.11
TriParseException.java 2 2 1.00 0 0.0 0 0.00
TypeEnum.java 7 6 0.85 0 0.0 1 0.14
Var.java 25 19 0.76 0 0.0 6 0.24

可見代碼中註釋比例偏低,但空行比例較高。類長度和其複雜度成正比。其中 \(PolyBuild\) 由於包含輸入狀態機,複雜度最高;\(Poly\) , \(Item\), \(Factor\) 三類的求導操作較複雜,複雜度次之。

方法複雜度

class OCavg WMC
poly.element.Const 1.23 16.0
poly.element.Element 1.00 7.0
poly.element.Tri 2.00 12.0
poly.element.TypeEnum 0.0
poly.element.Var 1.00 4.0
poly.Factor 1.55 28.0
poly.Item 2.28 64.0
poly.Poly 2.04 43.0
PolyBuild 8.37 67.0
PolyBuild.StringIterator 1.42 20.0
PolyBuildTest 1.00 4.0
Se 0.0
TriParseException 0.0
Total 265.0
Average 2.154471544715447 20.384615384615383

可見 \(PolyBuild\) 類的複雜度最高,約為平均值的 \(4\) 倍。而加入權重計算時,\(PolyBuild\)\(Item\) 類複雜度基本相同。

UML​ 類圖

uml

優點

類的內劇度高,類間邏輯關係清晰。\(UML\) 圖中從上至下為 \(Element\), \(Factor\), \(Item\), \(Poly\) 類,與邏輯關係相同。說明每個類都直接與其前驅類相關,而與其他類關聯度較低。

缺點

\(Item\), \(Poly\) 類的複雜度較高,尤其在求導時會進入遞歸,給調試造成困擾。

此處記錄了一些在開發過程中遇到的 \(Bug\) ,希望通過反思總結警醒自己。按照這些 \(Bug\) 的出現位置,我將其大致分為輸入運算以及輸出三個部分。每個部分都多多少少有一些 \(Bug\) ,證實了老師所說“不存在沒有 \(Bug\) 的代碼”。許多 \(Bug\) 十分隱蔽,需要大量測試才能發現。因此即使從邏輯上完備的測試了代碼,仍然需要更多測試,以檢測正確性。

輸入

輸入處理在這單元作業中最為繁瑣且易出錯,其原因主要在於識別非法輸入。正確識別合法的字元串並不困難,但一不小心很有可能將非法字元串當作合法輸入進行處理。結合被檢查出的 \(Bug\) ,我發現我對於字元出現的不同組合仍然考慮不全,因此將一些非法字元串誤認為合法進行解析。發現這個問題後,我在狀態機的轉移圖中檢查了所有可能出現的字元,這才避免了後續輸入上的 \(Bug\)輸入部分的 \(Bug\) 全部出現在解析類 \(PolyBuild\) 中。

非法的空白字元

在第一次作業中,最關鍵的 \(Bug\) 出現在非法的空白字元。由於允許的空白字元只有 \(\t\), 但 \(Java\) 中預設的 \(String.trim\) 方法會將字元串首尾的全部空白字元刪去。

位置

\(PolyBuild.main(String[])\)

樣例輸入

\f 1 + x

在調用 \(trim\) 方法時 \(\f\) 將被刪去。但 \(\f\) 的存在使整體字元串不合法。因此這樣的處理會將一些包含非法空白字元的輸入當作合法輸入處理。

解決方法

針對非法空白字元的解決方法主要有兩個:

  1. 在獲得輸入後進行合法字元篩查。若輸入中存在非法字元則直接報錯,不進行後續處理。這種方法簡單直觀,且向後相容性更強。當需求有變化時只需要在合法字元集中加入對應的字元即可。
  2. 在處理字元串時檢查,例如在狀態機中檢查每個位置的字元是否合法。這種方法的耦合度較高,因為合法字元集分散在狀態機各處,且判斷邏輯複雜,不易發現 \(Bug\) 。但優點在於,這樣的方法可以提高效率。輸入合法性不需要單獨判斷,而是在處理輸入的同時進行。

反思

這一問題的原因在於沒有區分空白字元的合法性,誤認為空白字元即合法。

輸入的簡並處理

在第一次作業指導書中就提到

表達式由加法和減法運算符連接若幹項組成…在第一項之前,可以帶一個正號或者負號

這樣的要求就使得第一項前的運算符變成可選項,但後續各項都可以解析為一個運算符(\([+-]\))和一個項的形式。為了統一每項的解析方式,我在處理字元串前將其 \(trim\) 併在其頭部加入 \(“+0”\) 。這樣的處理可以簡並以下情況

12 * x      -> +012 * x
+12 * x     -> +0+12 * x
+ 12 * x    -> +0+ 12 * x
+ +12 * x   -> +0+ +12 * x

同時考慮到如下的以 \(x\) 開頭的表達式

x ^ 2

我對字元串的首個非空白字元進行了特判。若其為 \(x\) 則只補全 \(“+”\) 而不輸出 \(“0”\) 。即

x ^ 2       -> +x ^ 2

位置

\(PolyBuild.main(String[])\)

樣例輸入

*x          -> +0*x

由輸入可以看出,原本非法的表達式被當作合法輸入進行解析。這一問題的出現即是因為事先沒有考慮到 \(“*”\) 作為表達式第一個字元出現的可能性。但從邏輯的角度看,\([+-*\^0-9x]\) 均為合法字元,應考慮到其在字元串開頭出現的可能性。

解決方法

最直接且行之有效的解決方法是加入對於 \(“*”\) 的特判。若 \(“*”\) 是字元串的首個非空字元,則報錯。但這樣的判斷顯然不夠簡潔。

另一種方法是判斷字元串的首個非空字元是否為操作符 \([+-]\) ,若非,則在字元串(\(trim\) 前)的開頭加入 \(“+”\) 。可以證明,這種方法可以完美的保留原輸入的合法性。

反思

這一問題主要是因為對於字元串開頭可能存在的字元考慮不全。在後面修改狀態機時,我在每個狀態都排查了所有合法輸入的可能,確保了狀態機的正確性。每行代碼都需要推敲與證明。

運算

運算主要是對於一個多項式完成加、減、乘、求導等操作。由於已經經過輸入部分的處理,因此不必在考慮非法輸入的問題,只需要對合法多項式進行對應操作即可。這部分的難度較低,但由於 \(Poly\) 類和 \(Item\) 類的底層數據結構均為集合,涉及到了拷貝問題,還是引發了一些問題。

淺拷貝

淺拷貝問題發生在構造新對象時。這一問題不僅存在於 \(Item\) 類和 \(Poly\) 類中,還存在於 \(Factor\) 類和 \(Element\) 子類中。由於構造方法中簡單的進行賦值,就造成了多個對象中的屬性指向同一個元素。操作一個對象時會改變其他對象的值。

位置

所有類的構造方法。

樣例輸入

記不清了…

解決方法

我最開始的解決方法是調用者負責 \(clone\) ,但構造函數一多,這樣的解決辦法很容易出錯。因此我後來將 \(clone\) 的調用轉移到構造方法中,雖然增加了複雜度,但代碼更加簡潔。

反思

這一問題的出現讓我直接把 \(Element\)\(Factor\) 改造成了不可變對象,每次調用其方法時必須申請新變數。這樣的措施雖然有效,卻沒有根除問題。事實上,這一問題在作業 \(3\) 的整個開發過程中一直存在,直到提交前才被解決。這幾乎是本項目設計上最嚴重的問題,應該從構架時著手思考。在以後項目開發的過程中,我會更加留意深淺拷貝的問題。

可變類型與不可變類型

在第三次作業中,由於出現了因數的嵌套,可變性的問題才顯得尤其突出。儘管我在編碼時儘量把類構造成不可變對象,且每個類都顯式重寫了 \(Object.equals(Object)\) 方法。但 \(Item\) 類與 \(Poly\) 類的構造方法中調用了本類的 \(mult()\)\(add()\) 方法,因而不能完全構造成不可變對象。對這一問題,我的解決方法是構造私有的 \(mult()\)\(add()\) 方法,供類內部使用。而外部調用 \(mult()\)\(add()\) 方法時通過公共方法 \(mult(Derivable)\)\(add(Derivable)\) 獲得新對象。這樣在類的內部,\(Item\) 類與 \(Poly\) 類是可變類型對象;在類的外部,\(Item\) 類與 \(Poly\) 類仿佛是不可變對象。但這決定了在 \(Item\) 類和 \(Poly\) 類的內部可能存在可變類型與不可變類型混淆的情況。

位置

\(Item.mult\) 方法(由於 \(Debug\) 過程較為複雜,具體哪個重載方法記不清了…)

樣例輸入

sin(sin(x))

錯誤輸出:

cos(sin(x))

解決方法

該問題的解決並不困難,只要定位到 \(Bug\) 並將返回值(新對象)賦值即可。難點主要在於 \(Bug\) 定位,因為帶嵌套的表達式會進入遞歸。我採取的是二分定位的方法,將程式分成若幹片段,在每個遞歸程式間設置斷點,定位到程式片之後再進入遞歸跟蹤斷點。

反思

這一問題是由於 \(Item\) 類和 \(Poly\) 類的可變性引起的。我在編碼時就預感到這樣的設計可能導致 \(Bug\) ,但還是沒有將所有情況考慮完備。如果將 \(Item\) 類和 \(Poly\) 類完全設計為可變類型,雖然在調用者處需要顯式調用 \(clone\) 方法,但卻解決了類型可變性的問題。只是這樣的修改成本較大,在後期我沒有採用這樣的方法。

輸出

輸出部分的難點集中在判斷和調用子類的 \(toString()\) 方法。比如一個 \(Item\) 類包含兩個 \(Factor\)\(1\)\(x^2\) 。應輸出 \(+x^2\) ,但單獨調用每個 \(Factor\)\(toString()\) 方法並拼接將會生成 \(+1*x^2\) 。由此可知,輸出的問題可能出現在優化中。

表達式因數的優化

在優化作業 \(3\) 時,由於涉及到合併同類項,\(Item\) 類的輸出會包含表達式因數。這是一個十分複雜的過程,因為表達式因數的括弧在某些情況下是可以省略的(如果表達式因數只有一個 \(Factor\))。問題出現在表達式因數的第一項包含省略時,即形如 \(+x^2\) 的情況。若表達式因數的括弧被省略,那麼在這個 \(Item\) 中將會出現非第一項被省略的情況,即形如 \(4*x^5*+sin(x)\)

位置

\(Item.toString()\)

樣例輸入

不考慮求導過程,即調用

System.out.println(new PolyBuild(string).parsePoly())

時,若輸入

4*x^5*5*sin(x)-4*x^5*4*sin(x)

將會產生輸出:

4*x^5*+sin(x)

解決方法

在合併同類項時,加入判斷表達式因數是否可以轉發為其他因數。若表達式因數只含一個 \(Item\) ,則將其轉化為 \(Item\) 與本類相乘。

反思

為了使優化更加簡單,不易出錯,應將優化過程前移至最開始可以優化的位置。以合併同類項為例,在項與表達式因數相乘時即可開始優化。如果在運算完成後在進行優化,一方面需要訪問多個類的私有方法,破壞了封裝性;另一方面操作過於複雜,容易引發錯誤。

互測策略

一般來說,我測試其他人代碼分為三個步驟:

  1. 利用針對自己代碼的測試集進行測試
  2. 閱讀代碼,針對性測試
  3. 利用腳本大量測試

測試集測試

一般情況下,這種測試方法只能檢查程式的基本表現。由於測試集是針對我的代碼編寫的,儘管從我的編碼邏輯上做到了覆蓋,但並不一定能覆蓋他人的代碼。因此這一輪測試只是檢查他人代碼能否完成最基本的求導操作。

針對性測試

這個階段我會閱讀對方代碼。閱讀重點放在輸入和輸出的處理,因為運算部分比較簡單。如果有比較明顯的邏輯錯誤,在這個階段就可以暴露出來。如果閱讀一遍沒有發現問題,我會查看運算部分的邏輯,同時編寫測試集進行測試。但是這裡的測試集一般不能做到覆蓋,只是針對頂層的邏輯進行檢查,否則沒有時間測試更多代碼。

腳本測試

腳本測試一般和閱讀代碼同時進行,因為測試量較大,運行時間長( \(5000\) 個測試樣例一般需要 \(15\sim20\) 分鐘)。如果前兩個階段都沒有發現問題,腳本生成的隨機輸入可以全面檢查程式的正確性。大部分 \(Bug\) 都會在這個階段被髮現。

Creational Pattern

在開始寫這一單元項目的時候,我對設計模式還沒有很全面的瞭解,因此沒有運用。但後來查閱相關資料發現原型模式很適合我的項目。由於我的所有類都繼承自 \(Derivable\) 介面,而且所有類都重寫了 \(clone\) 方法(由於 \(Derivable\) 介面繼承了 \(Cloneable\) 介面,因此必須重寫)。我認為重構可以在 \(Derivable\) 介面中加入預設的 \(clone\) 方法,並子類的構造方法融入 \(clone\) 方法中。這樣不僅可以加快程式的運行速度,還能更嚴格的保證對象的不可變性。

\(P.S.\) 不知道 Applying Creational Pattern 是不是設計模式的意思…)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 事件分發 之前講述了事件如何綁定在 上,那麼具體事件觸發的時候是如何分發到具體的監聽者呢?我們接著上次註冊的事件代理看。當我點擊 按鈕時,觸發註冊的 事件代理。 為`click nativeEvent dispatchEvent(topLevelType, nativeEvent) _interac ...
  • 下麵看下正則表達式實現手機號中間4位數隱藏或者只顯示末尾四位數 ? 1 2 // 匹配手機號首尾,以類似“123****8901”的形式輸出 '12345678901'.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); ? 1 2 // 匹配手機號首尾,以類似“ ...
  • 最近這些年,隨著前端應用技術突飛猛進,產生了很多新的前端框架,當然也引入了數不勝數的前端技術概念,前端不在是早期Web Form的拖拉處理方式,也不再是Ajax+HTML那麼簡單,隨著前端技術的發展,前端的JS越來越重要,也越來越複雜,而為了開發的方便,引入了很多可以對JS+CSS進行編譯的框架,而... ...
  • 前言:IE一直是特殊的一個瀏覽器,我們可以使用一些方法來指定樣式表只在IE瀏覽器下被載入。 IE9以及低於IE9版本 : 可以使用條件註釋語句來載入特定於ie的樣式表。如下所示,使用外部css3樣式表。 ? 1 2 3 <!--[if IE]> <link rel="stylesheet" type ...
  • 十一大行為型模式之九:備忘錄模式。 簡介 姓名 :備忘錄模式 英文名 :Memento Pattern 價值觀 :凡事要有備份 個人介紹 : Without violating encapsulation,capture and externalize an object's internal st ...
  • 通過第一單元的學習,首先是對面向對象程式設計思想有了一個大致的認識。其次對於面向對象語言的各種概念和機制有了大致的瞭解併進行了應用(如類、方法、繼承、介面、try-catch機制等)。通過自己課下練習、與同學討論和在課上通過老師對作業的總結講解,能夠明確感受到這樣一種思想與之前接觸的面向過程的一些程... ...
  • OO第一單元(求導)單元總結 這是我們oo課程的第一個單元,也是意在讓我們接觸瞭解掌握oo思想的一個單元,這個單元的作業以求導為主題,從一開始的加減多項式求導再到最後的嵌套多項式求導,難度逐漸提高,編程思想也逐步深入面向對象,如果說前兩次作業還可以用C語言的面向過程的思想去對付,那麼第三次作業可以說 ...
  • 面向方法的數據集緩存 使用了autofac做為ioc容器,使用Autofac.Extras.DynamicProxy2作為方法攔截器,緩存面向方法,直接在方法上添加CachingAttribute特性即可。 緩存註冊流程 1. 關於方法攔截器 1. 緩存名稱設計 1. 添加ioc 1. 添加攔截器與 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...