seansie's blog

物件導向四大原則 封裝 抽象 繼承 多形 介紹 | seansie blog

甚麼是物件導向

簡而言之,物件導向是一種抽象化的概念。在當今軟體開發需求快速變遷的時代,物件導向所帶來的便利性至關重要。

其實,「抽象」並不可怕,它反而能簡化問題,讓開發者專注於真正核心的業務邏輯。

舉個例子,其實物件導向就像日常生活的包裝,沒有了他其實也沒什麼大不了的,甚至對於生產者來說更方便,因為不用準備包裝的成本。

  • 但是對於消費者來說,包裝對於日常使用的方便性是非常重要的,它可以保護內部重要部分(也就是對應封裝)
  • 簡化功能,例如像是鋁箔飲料,保留一個孔洞提供吸管插入(對應抽象)。
  • 以包裝為藍本進行客製化,像是客製化便當之類的,對應繼承
  • 最後,不同的便當,可以透過相同的"取餐流程"交給顧客,例如取餐號碼牌,這就是多型

而 剛剛的比喻中,封裝、抽象、繼承跟多形這四個重要概念是物件導向的基石,讓開發者能更靈活的面對各種變動的需求。

封裝與抽象

這兩個要一起說。因為互為因果,為了抽象所以封裝

再舉個例子,大家使用電腦的時候,應該都是用鍵盤滑鼠操作吧,而不是打開主機板,用電流來操作裡面的晶片,這種如此荒唐的操作方法。

為甚麼不行呢? 因為裡面的電路與晶片非常脆弱阿,一不小心搞到短路還是怎樣,就會整台報銷了。

因此這些東西會被一個外殼保護起來,這個外殼在物件導向的世界中叫做封裝。

對了,那物件中哪個東西就如同電腦主機板一樣脆弱呢?

通常是資料,因為可以任意修改,沒辦法驗證,比如說把年齡設成負數,或是把金額設成無限大,這些不合理的數值,都有可能導致程式出錯甚至崩潰。因此,我們會將資料封裝起來,不讓外部直接存取。

那要如何存取被封裝的資料呢?這時候就輪到成員函數 (Member function) 登場了。方法就像是電腦的鍵盤滑鼠,提供一個安全的介面,讓外部可以間接地操作資料。在方法中,我們可以加入邏輯判斷,確保資料的正確性,例如:在設定年齡的方法中,我們可以檢查傳入的值是否為正整數;在設定金額的方法中,我們可以檢查是否超過預算上限。

抽象 (Abstraction) 則是將複雜的內部實作細節隱藏起來,只暴露必要的資訊給外部。使用者不需要知道電腦內部是如何運作的,只需要知道如何透過鍵盤滑鼠來操作電腦即可。同樣地,使用者也不需要知道物件內部的資料是如何儲存和處理的,只需要知道如何透過物件提供的方法來操作物件即可。

所以,封裝的目的是為了保護資料,而抽象的目的是為了簡化操作。兩者相輔相成,共同構成了物件導向程式設計的基石。透過封裝和抽象,我們可以建立出更安全、更易用、更易維護的程式。

C++ 中的實作

class myClass{
	public:
		
	private:
		
}

裡面的 publicprivate 代表裡面對應的資源的可視性 (visibility) 。

如果是 public 就可以被外界存取,而 private 只能被內部 (在 {} 之內的函數)存取。

有沒有發現 publicprivate 剛好對應到對外的抽象與對內封裝,並且剛剛又提到資料因為更改不可驗證而很脆弱,因此要被封裝起來,所以 private 裡面基本上都是儲存資料。

反觀 public 是給外部用的,對應到向外的抽象介面,也就是公開的成員函數,函數是介面的一種。

設計的原則是,如同電腦的鍵盤滑鼠一樣,當使用者使用介面的時候,只需要知道意圖,而裡面的細節是介面要自己完成的,要盡量隱藏瑣碎的細節。

順帶一提,還有一些抽象介面的準則要遵守,例如。

  • 高內聚性: 每個公開方法應該負責單一且明確的任務。例如,與其提供一個只取得半徑的 public 方法,然後讓使用者自行計算球體面積,不如直接提供一個 calculateSphereArea() 方法,讓使用者可以直接取得球體面積,提高程式碼的可讀性和易用性。
  • 最少暴露原則 (Principle of Least Privilege): 只有在真正需要被外部存取時,才將成員設為 public。這有助於降低程式碼之間的耦合度,並提高程式碼的可維護性。

繼承與多形

繼承:客製化便當的基礎

如果說物件導向是一家便當店,那麼繼承就是推出「客製化便當」的好方法。

想像你的便當店推出了一款「招牌雞腿便當」,裡面有雞腿、白飯和三樣配菜。為了滿足更多顧客的需求,你決定推出「客製化便當」服務。客人可以選擇:

  • 更換主菜:例如把雞腿換成排骨
  • 加點配菜:例如加一顆滷蛋或一份燙青菜

這就是繼承!你以「招牌雞腿便當」(父類別) 為基礎,創造了「客製化雞腿便當」(子類別)、「客製化排骨便當」(另一個子類別) 等,它們都繼承了「招牌雞腿便當」的基本內容(白飯和三樣配菜),並可以添加或修改一些內容。

好處是什麼?當然是省時省力!你不需要為每一種客製化便當從零開始設計菜單和製作流程,只要基於「招牌雞腿便當」做調整就好,這就是程式碼複用

C++ 中的實作

C++

class SignatureChickenBento : public Bento { // 招牌雞腿便當 繼承自 Bento
public:
    SignatureChickenBento() {
        setName("招牌雞腿便當");
        mainDish_ = "雞腿";
        sideDishes_ = {"配菜A", "配菜B", "配菜C"};
    }
    // ... 其他方法
};

class CustomizedBento : public Bento { // 客製化便當 繼承自 Bento
public:
    CustomizedBento(const std::string& mainDish) {
        setName("客製化便當");
        mainDish_ = mainDish;
        sideDishes_ = {"配菜A", "配菜B", "配菜C"}; // 預設配菜
    }

    void addSideDish(const std::string& sideDish) {
        sideDishes_.push_back(sideDish);
    }
    // ... 其他方法
};

在這個例子中,SignatureChickenBentoCustomizedBento 都繼承自 Bento 類別。SignatureChickenBento 設定了預設的主菜和配菜,而 CustomizedBento 則允許顧客自訂主菜和加點配菜。

多型:不同便當,統一的處理流程

現在,讓我們來談談多型。如果說繼承讓「客製化」成為可能,那麼多型就是讓便當店可以「高效運作」的關鍵。

想像一下,你的便當店裡同時有「招牌雞腿便當」、「客製化雞腿便當(加滷蛋)」、「客製化排骨便當」等等不同的便當訂單。但對於負責將便當交給顧客的店員來說,他不需要去記住每一種便當的具體內容,他只需要知道:

  • 每一個便當都有一個取餐號碼。
  • 叫到號碼的顧客,憑號碼牌來取餐。

至於便當裡具體裝了什麼?那是廚房製作便當時該煩惱的事。這就是多型

不同的便當 (不同的物件) 對「交給顧客」這個指令 (方法) 可以有不同的反應 (實作)。「招牌雞腿便當」和「客製化排骨便當」的內容當然不同,但店員不需要知道這些細節,他只需要知道「憑取餐號碼交給對應的顧客」這個統一的流程就好。

換句話說,多型讓你可以用統一的方式處理不同的物件。 就像店員可以用相同的取餐流程處理所有類型的便當,而不需要針對每種便當都制定一套不同的流程。

C++ 中的實作 - 多型的體現

雖然 C++ 的語法並不能直接反映出「取餐流程」,但我們可以透過一個概念性的函式 deliverBento 來理解多型的應用:

C++

// 假設 deliverBento 是一個負責將便當交給顧客的函式
void deliverBento(Bento* bento) { 
    // 根據便當的類型做不同的處理,例如列印出便當內容
    std::cout << "將" << bento->getName() << "交給顧客。" << std::endl;
    
    // 這裡的取餐號碼邏輯只是一個示意的虛擬碼
    int orderNumber = generateOrderNumber(bento); // 假設這是一個可以取得便當取餐號碼的函式
    std::cout << "取餐號碼: " << orderNumber << std::endl;
}

int main() {
    SignatureChickenBento chickenBento;
    CustomizedBento customizedBento("排骨");
    customizedBento.addSideDish("滷蛋");

    deliverBento(&chickenBento);       // 將招牌雞腿便當交給顧客
    deliverBento(&customizedBento);    // 將客製化排骨便當交給顧客
    return 0;
}

在這個例子中,deliverBento 函式可以接受任何 Bento 類型的指標 (包括 SignatureChickenBentoCustomizedBento)。由於 getName() 是一個公開方法,而且在 Bento 類別中存在,因此 deliverBento 函式可以呼叫它,而不需要知道 bento 指標實際指向的是哪種類型的便當。這就是多型的體現:對不同的物件執行相同的操作,但會產生不同的結果

而因為透過 Bento 指標來操作,因此可以當成同一種類型的物件。

當然,如果需要根據便當的具體類型執行不同的操作,我們可以使用 dynamic_cast 或虛擬函式等進階技術,但這已經超出了目前討論的範圍。

總結:

  • 繼承 讓你基於現有的類別創建新的類別,實現程式碼複用和客製化。
  • 多型 讓你用統一的方式處理不同的物件,簡化程式碼邏輯,提高程式碼的靈活性和可擴展性。

總結

封裝、抽象、繼承和多型是物件導向程式設計的四大基石。它們相互配合,使得我們能夠建立出更模組化、更可複用、更易維護和更具擴展性的軟體系統。在面對日益複雜的軟體開發需求時,物件導向程式設計提供了一套強大的工具和方法,幫助我們構建出高品質的軟體。透過深入理解和靈活運用這些概念,我們可以開發出更優雅、