題目集4~6的總結

22207330-曾融發表於2024-11-23

目錄
  • 一.前言
      •  nchu-software-oop-2024-上-4 ~知識點
      •  nchu-software-oop-2024-上-5 ~知識點
      •  nchu-software-oop-2024-上-6 ~知識點
  • 二.設計與分析
      • 一.答題判題程式-4
        • 1.繼承
        • 2.多型
      • 二.家居強電電路模擬程式-1
        • 1.類的設計
        • 2.抽象類
      • 二.家居強電電路模擬程式-2
        • 1.物件導向設計原則——單一職責原則
        • 2.類的設計之繼承與多型
  • 三.踩坑心得
      • 1.執行中除0
      • 2.精度問題
      • 3.考慮不周
  • 4.改進建議
      • 1.語句與深度
      • 2.子父類
  • 5.總結

一.前言

  這三次大作業的難度總體而言比以往簡單了些,但是對類的設計要求更高了。或許是有前三次大作業的基礎在,這三次大作業雖然難度降低,但收穫卻大得多。話不多說,上知識點。

 nchu-software-oop-2024-上-4 ~知識點

  • 繼承
  • 多型
  • 正規表示式

 nchu-software-oop-2024-上-5 ~知識點

  • 繼承與多型
  • 抽象類
  • 類的設計
  • 物件導向設計原則

 nchu-software-oop-2024-上-6 ~知識點

  • 繼承與多型
  • 類的設計
  • 抽象類
  • 物件導向設計原則

  在這三次作業中,4是答卷題,5和6是電路題。答卷題不必多說,已經總結過一次了,這次只是多加了個繼承和多型,總體而言比較簡單。然後是電路題,這題花了我很多時間去思考類的設計,這也是我覺得的電路題的精髓,這會在之後總結中詳細說明。

  接下來我會按照設計與分析、採坑心得、改進建議、總結四部分進行這次PTA題目集4~6的總結。

二.設計與分析

對於題目集4~6第一題的分析

一.答題判題程式-4

  這道題新增了一個多選題和填空題。在初步學習瞭解繼承和多型後,其做題的方法就顯而易見了,甚至不怎麼需要改程式碼。

  以下是我第四次作業的類圖。

1.繼承

  先前就聽聞物件導向最顯著的特性有“封裝、繼承、多型”三個,而寫這道題目前就學習並瞭解了這三個特性,印象裡寫這題是很順利的。

  首先是繼承:

  繼承是從已有的類中派生出新的類,新的類能吸收已有類的資料屬性和行為,並能擴充套件新的能力。Java繼承是使用已存在的類的定義作為基礎建立新類的技術,新類的定義可以增加新的資料或新的功能,也可以用父類的功能,但不能選擇性地繼承父類。

  而這種特性使得我們設計多選題和填空題的難度大大降低,這兩者皆繼承Topic類就行。

class Topic{
    ......;
}
class MCQ extends Topic{//多選題類
    ......;
}
class Fill extends Topic{//填空題類
    ......;
}

  繼承避免了對一般類和特殊類之間共同特徵進行的重複描述。

2.多型

  本次大作業的另一考點正是多型。多型是個很好用的東西,它不止被運用在第四次大作業中,在第五六次大作業中它也發揮著至關重要甚至不可或缺的作用。

  關於多型:

  在一些資料中,多型被定義為“為不同資料型別的實體提供統一的介面”。計算機程式執行時,相同的訊息可能會送給多個不同的類別之物件,而系統可依據物件所屬類別,引發對應類別的方法,而有不同的行為。

  簡單來說,所謂多型意指相同的訊息給予不同的物件會引發不同的動作。

  大家不妨想一想,新增的多選題和填空題與固定題型有什麼區別?除開答案的不同,其實最顯著的一個區別就是它們的判題方法不一樣。只要利用多型寫下不同題型的不同判題方法,其實這次大作業也就差不多寫完了,不需要改什麼程式碼。

class Topic{
public String judge(String a){//透過傳入的答案a,跟正確答案進行比對
    ......;
    }
}
class MCQ extends Topic{//多選題類
    public String judge(String a){//透過傳入的答案a,跟正確答案進行比對,但比對方法與上面不同,是判斷是否符合ABCD
    ......;
    }
}
class Fill extends Topic{//填空題類
    public String judge(String a){//透過傳入的答案a,跟正確答案進行比對,但比對方法不同,是判斷是否為子字串
    ......;
    }
}

二.家居強電電路模擬程式-1

1.類的設計

  這次大作業的題目很早就釋出在群裡了,比起迅速動手寫,我是先琢磨起了類的設計:該怎麼設計類才可以使各元器件順滑地連線在一起?怎麼設計類才能使各元器件有序執行自己的那一套方法?怎麼設計類才能為後來的迭代留下新增功能的空間?……

  我花了很多時間去想這些事情,甚至產生了些現在看來不必要的想法:一開始我被引腳牽引了思路,覺得既然設定了引腳,那麼就應該有一個引腳類,然後引腳類的例項物件中可以存放元器件,這樣就可以把各個元器件連線起來。

  後來我又想,為什麼不乾脆設計個電路類?我不需要設定引腳,也不需要關注各個元器件的擺放位置,我只需要一個電路類的例項物件,這個物件裡應有一個容器存放這條線路中的所有元器件,這樣,各個元器件也同樣可以連線起來。

  我越想,就越覺得比起引腳類,電路類顯然才是正道。

  或許你會問,設計的電路類確實解決了串聯,但並聯該怎麼辦呢?這個問題我早在電路第一次大作業就想好了解決方法:並聯顯然可以看作是多個串聯路線的結合體,而串聯路線也可以看作是一個“元器件”,這個“元器件”存放在並聯的例項物件的容器中,其實與串聯電路例項物件的容器中存放的元器件並無區別。

  或許有人要問了,若主路線中有並聯,並聯中又有並聯該怎麼辦呢?這個問題其實跟上面這段很相似。同樣地,我們可以把並聯也看做是一個“元器件”,這個“元器件”存放在主路線中,等程式執行到需要對這個並聯電路賦值時,我們又可以把並聯電路拆成串聯路線,而拆出的串聯路線裡又有並聯電路這個“元器件”,我們再做相同的賦值操作即可。這段話聽起來很繞,但其實如果你這題拿了滿分,我猜,你的思路大抵跟我是一樣的。

  我對類的設計就是在這樣一問一答中進行的,雖然我在此只列出了兩個問題,但實際上當時我內心冒出的問題比這要多得多得多。我從最開始的暈頭轉向,到後面梳理清邏輯,早在第一次大作業就考慮好後續2、3並聯電路迭代的計算問題,這一切都要歸功於類的設計

以上是我的類圖。

我考慮類的設計時大概是五一假期,當時蔡老師還沒有給出他對類的設計建議,所以上述一切是我獨自思考後的結果。
後來過了半個月,蔡老師釋出了大作業,幾天後又提出了他自己對類的設計,然後我發現我們類設計的重合度至少在口頭表達上是很高的,同樣的元器件類與Line類,以及繼承元器件類等等,關鍵的幾個點都能對得上,這更加強化了“類的設計”在我心中的重要性。

這次大作業在我看來是出得特別特別好的,它給足了我思考的時間,也促成了我第一次自發主動地去設計自己的類,更讓我理解了設計好類有多麼的重要。

2.抽象類

  關於抽象類:(這裡引用了他人的總結與定義)

  在物件導向的概念中,所有的物件都是透過類來描繪的,但是反過來,並不是所有的類都是用來描繪物件的,如果一個類中沒有包含足夠的資訊來描繪一個具體的物件,這樣的類就是抽象類。在Java中,如果一個類被abstract修飾則稱為抽象類。抽象類往往用來表徵對問題領域進行分析、設計中得出的抽象概念,是對一系列看上去不同,但是本質上相同的具體概念的抽象。

  本次大作業中,有一些類我也設定成了抽象類:

abstract class Device{
    String name;//編號
    double U;//電壓
    int state;//狀態
    double R=0;//電阻
    double I=0;//電流
    ......
}
abstract class Lamp extends Device{
    ......
}

二.家居強電電路模擬程式-2

1.物件導向設計原則——單一職責原則

  關於單一職責原則:(此處引用了他人的定義與總結)

  單一職責原則又稱單一功能原則,物件導向五個基本原則之一,它規定一個類應該只有一個發生變化的原因。所謂職責是指類變化的原因。如果一個類有多於一個的動機被改變,那麼這個類就具有多於一個的職責。而單一職責原則就是指一個類或者模組應該有且只有一個改變的原因。

  本次大作業我的另一改變就是主動往“單一職責原則”上接近。無論是上個學期的C語言還是這個學期的Java,都一再強調過單一職責原則,但出於某種難以描述的心態,我幾乎沒注意過這個原則,幾乎每段程式碼都是長篇大論的。

  本次大作業我不敢說自己完全遵守了“單一職責原則”,甚至有部分方法仍然是“長篇大論”,但起碼可以說,比起以前,我在儘量使自己更加註意這一點。

  話不多說,直接上圖。

這是答題判題迭代4的資料,可以看到,最高複雜度有21,平均複雜度也有3.09

這是我電器題1的資料,最高複雜度為9,平均複雜度為2.21

這是電器2的資料,最高複雜度為10,平均複雜度為1.94

  顯然,無論是最高複雜度還是平均複雜度,電路題較答卷題都有顯著降低。即使這有題目不同的原因,也並不能完全跟“單一職責原則”掛鉤,但也許或多或少能說明我有儘量往這方面接近,這使我收穫很多。

遵循單一職責原的優點有:

1、類的複雜性降低。
2、可讀性提高。
3、耦合度較低,所以更容易修改。
4、擴充性更好。

  關於這個複雜度方面,其實我的還是有點高了,這點我會在後文的“改進建議”裡進行一個反思。

2.類的設計之繼承與多型

我這個標題的表述或許並不嚴謹

  電路題大作業的計算,知道了“串聯電路電流相同,並聯電路電壓相同”的原理後,就可以利用繼承和多型實現,計算電流電阻電壓的方法,會根據串並聯電路的不同而不同,這正是方法覆蓋:子類繼承父類並重寫父類中已經定義的方法,以實現不同的行為。

  關於多型:(此處引用了他人的定義與總結)

  簡單來說就是一個物件有多種形態,具體點說多型就是父類中的屬性和方法被子類繼承後,可以具有不同的資料型別或不同的行為,使父類中的同一個屬性和方法在父類與各個子類具有不同的含義。

  知道了這個後,我們計算前就可以進行方法覆蓋,這樣最後就可以從頭到尾用同一個方法名進行計算。下面是我的部分程式碼解釋:

public void working(){
    queen.working();//主路線進行工作,也就是計算賦值
    Output.show(device);
}
public void working(){
    ......;//略
    for (Map.Entry<String, Device> entry : device_self.entrySet()) {
        entry.getValue().setI(I);//進行計算賦值
    }
}

  多型可以讓程式碼的擴充套件性更強,更靈活,也可以提高程式碼的複用性。

三.踩坑心得

這一部分會著重講第六次大作業的電路題2

1.執行中除0

  在測試自己寫的測試樣例進行摸索時,我發現了一個問題,那就是有時並聯電路的電阻會突然顯示為“Infinity”,這表示電阻為無窮。然後我就開始疑惑了,為什麼會顯示電阻為無窮呢?隨後我發現了一些問題:

public void r(){
        ......;//略
        for (Map.Entry<String, Device> entry : device_self.entrySet()) {
            resistance+=1.0/entry.getValue().getR();
        }
        R=1/resistance;
        ......;//略
    }

  這段程式碼乍一看好像還正常,其實有大問題。對於並聯電路,在計算電阻的倒數時,我只進行了加的操作,沒有考慮這個倒數可能為0的情況。(雖然測試點中似乎也沒有倒數為0的情況,因為這種情況在正常電路中不會出現,它會出現在短路電路中,或是全是開關的電路中)

  意識到自己沒考慮到除0的情況後,我進行了判定,如下:

public void r(){
        ......;//略
        for (Map.Entry<String, Device> entry : device_self.entrySet()) {
                if(entry.getValue().getR()!=0)
                    resistance+=1.0/entry.getValue().getR();
        }
        if(resistance==0)
            R=0;
        else
            R=1.0/resistance;
        ......;//略
    }

  關於除0這個情況,其實不止出現在了我並聯電路的計算中,它還出現在了其他地方,因此,敲程式碼時要細心一點,考慮周到一點,不然後面出現了一些未知的問題,很難找到原因。

2.精度問題

  或許有人與我一樣,寫到94分就出現了2個過不去的測試點。

  在與同學進行討論後,我知道了自己問題所在:

  好像沒什麼問題對吧,但其實這個B,就是燈要輸出200。隨後,我讓這個燈輸出它的電流電阻電壓,分別是21.999999999999996,10.0,219.99999999999997。
  這個時候就出現了問題了,因為我計算過程中運用了很多乘法和除法,精度或多或少都有損失,比如這個電流21.99999多,其實原本應該是22.0的,這使我吃了大虧,花了很多時間尋找問題。

3.考慮不周

  在第五次大作業中,我遇到了四個測試點過不去的情況。因為本次大作業幾乎不涉及計算,所以我考慮一會,更改了一些地方,最終發現是分檔調速器的檔位問題。
  其實這是一個很小的問題,因為檔位是在0到3之間變化,所以檔位不應該超過這個範圍。但我一開始懷有僥倖心理,覺得既然老師說不會有錯誤資訊的輸入,所以我想,應該也不至於升檔升到超出界限才是,就沒注意這一部分。最後把檔位控制在0~3後就透過了測試點。
  寫作業時應該細心一點,在寫的過程中就應該多去考慮意外情況,而不是覺得無所謂。當自己考慮周全時,其實會省去很多麻煩。

4.改進建議

1.語句與深度

  在上文中,每次大作業我都用SourceMonitor進行了檢查並貼出了資料,雖然比起以往,最高複雜度及平均複雜度都有所降低,但是令我不解的是,有一個東西一直很高,那就是Block Histogram顯示的資料(右下角紅色直方圖那部分)。我曾看到別人程式碼檢測的資料,有一些人這一部分最高都只有20幾,而我卻達到了120幾。
  經瞭解,Statements指的是語句數目,分支語句if,迴圈語句for、while,跳轉語句goto都被計算在內;而Block Depth指的是函式深度,指示函式中分支巢狀的層數。
  於是我觀察了自己的程式碼,幾乎全篇都是if,else的結合體,還有一兩個方法是非常非常繁瑣的,一層套一層,if-else裡面長篇大論。我搜尋了一些網友建議的減少if-else的方法,有表驅動,職責鏈模式,註解驅動,事件驅動,有限狀態機,Optional,Assert,多型,列舉……這裡面有很多我幾乎完全不瞭解的,意味著未來還有很多的學習之路要走,還得多加學習。

2.子父類

  眾所周知,子類呼叫父類方法可以使用“super”關鍵字,那麼父類可以呼叫子類的方法嗎?如何實現?
  這次大作業,我所設計的所有類都有個共同的父類(元器件device),所有類的例項物件都當作元器件儲存,即類似於:

TreeMap<String, Device> device = new TreeMap<>();//存放所有元器件
Switch k=new Switch(name);
device.put(name,k);

  後來當我執行時,發現了一點問題,那就是在我所寫的子類中有一些父類沒有的方法,但我是將子類當作父類存的,這些子類獨有的方法就無法呼叫。我想了一個不是很好的方法,那就是在父類中新增同名的方法,只不過方法體體為空,其實換一句話說可能就是多型吧。這有一個很明顯的壞處,那就是父類的方法會越來越繁雜,父類中的方法不是所有子類都需要的,於是子父類之間的關係變得混亂。
  我於網上了解了一些父類呼叫子類方法的方案,但都需要在父類中多寫一個方法,並沒有達到我所期待的效果。所以我想,其實父類與子類之間的關係應該一開始就構思得好一點,讓所有子類都需要的方法放到父類中,而不是有的子類需要這個方法有的不需要,這是個可以改進的地方。

5.總結

收穫:
  • 對繼承和多型的瞭解加深。
  • 能更好地設計類。
  • 單一職責原則。
  • 學習了treemap的使用及其部分方法

  比起前三次大作業,這三次大作業給我的感受就是應該考慮程式碼的優劣,該怎麼寫更利於後續可能誕生的改變,而不是僅僅是把題目寫出來就萬事大吉。從讀題思考,到類的設計、具體方法、如何實現等,都需要經過很多的考量,如果能獨立思考完成這些部分,自己的思維和創造力都會有所提升,當然,多與同學交流也是大有益處的。
  然後,在寫題目時,也要多考慮意外情況。雖然電路題大作業並沒有錯誤輸入,但是卻也有一些需要考慮的點,比如分檔調速器的檔位問題,它不可能超出自己的檔位,也不可能檔位為負等等。如果小的細節沒有考慮到,最後找錯誤就會非常麻煩。
  最後,本次大作業給我帶來了很多提升,同時也讓我意識到自己還有很多需要改進的地方。希望在日後的學習中,自己能越來越好。