Louis Better than before

淺談物件導向的基本概念(I)

“Object-oriented programming (OOP) is a programming paradigm based on the concept of “objects”, which can contain data and code: data in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods). Many of the most widely used programming languages (such as C++, Python, etc.) are multi-paradigm and they support object-oriented programming to a greater or lesser degree, typically in combination with imperative, procedural programming. Significant object-oriented languages include: C++, Python, Perl, MATLAB … etc. - wiki”

基本概念

物件的觀點

從實作的視角看待物件,可被視爲是一種處理資料的智慧方法,透過描述問題領域中狀態的資料,然後增加處理資料的方法(必要的行為所需要的)。從概念性的視角看待物件,可被視為是具有責任的一個實體(instance),而這責任定義了物件的行為。透過概念性的視角有助於關注物件的意圖行為,而不是物件如何實作,也能幫助免於過早操心實作細節,而將這些實作細節隱藏起來,僅需要關注物件的公開介面。

編寫功能或需求

編寫功能或需求的方式,可藉由建模的方式進行,有助於提供程式碼的可理解性,而容易理解將使程式更易維護。但是這種方法並不總是有助於程式碼因應所有可能遇到的變化,因為這方法可能存在兩個問題:低內聚以及緊耦合。

內聚性(cohesion)在一個副程式中,鄰近的操作都是緊密相關的關係,或可稱為清楚度,因為副程式(或類別)中的多個操作關聯越緊密,就越容易理解其含義。換句話說,一個類別低內聚,指的就是它的任務很多而且互不相關,程式碼經常看上去像是令人疑惑的一大團混亂。耦合性(couple)指的是兩個副程式之間關聯的緊密程度。

一般而言,軟體開發的目標應該是建立內部完整(高內聚, high cohesion),而與其他副程式之間的關聯性則是小巧、直接、可見、靈活的(鬆耦合, loose coupling),可避免在程式碼的某個地方修改了一個函式或一個資料,後來卻對程式碼的其他地方造成了意想不到的影響。在維護或除錯的過程中,改正bug只需要花很少的時間,維護和除錯的絕大多數時間都被用於努力了解程式碼的運作機制、尋找bug和防止出現不良副作用,真正的改正時間卻相當短。這個時候,使用功能分解對於軟體開發和維護工作產生極大的影響。

物件導向之概觀

OOP中的關鍵概念是資料抽象化(abstraction)、繼承(inheritance),以及動態鏈結(dynamic binding)。

  • 資料抽象化(abstraction): 可以定義將介面與實作分離的類別 “Data abstraction: define classes that separate interface from implementation.”
  • 繼承(inheritance): 可以定義出類別作為模型,以捕捉相似型別之間的關係 “Inheritance: define classes that model the relationships among similar types.”
  • 動態鏈結(dynamic binding): 能讓開發者使用那些型別的物件,不去在意它們之間差異的細節 “Inheritance: use objects of these types while ignoring the details of how they differ.”

物件導向範型

物件導向範型以物件概念為中心,一切都集中在物件上。編寫程式碼時是圍繞物件而非函數進行組織的。物件可定義為含有資料和用來存取、處理資料行為的項目,其優點在於可定義自己負責自己的事物,以及物件中的資料能夠告訴它自己的狀態如何,而與物件中的程式碼能夠使它正確工作。

一種編寫物件效率高的方式是透過類別(class)讓所有的物件與一組方法連結起來。類別就是對物件行為的定義,它包含以下內容的完整描述:

  • 物件所包含的資料元素
  • 物件能夠操作的函數
  • 存取這些資料元素和函數的方法

程式碼要獲得一個物件時,需要建立一個類別的實體(instance),稱之為實體化(instantiation)。此外,倘若需要一個能包容多種具體類別的一般類別,就需要建立一個抽象類別(abstract class)代表一個概念特定的、不變的實作,這種關係稱之為繼承(inheritance)。

物件具有責任且都自己負責自己,所以有一部分不需要暴露給其他物件,也有部分可以被其他物件存取。在物件導向系統中,可存取性主要分為以下幾種類型:

  • 公開(public)- 任何物件都能看見
  • 保護/朋友(protected/friends)- 只有這個類別及其衍生類別的物件能夠看見
  • 私有(private)- 只有這類類別的物件能夠看見

其中,private以及protected存取類型,可描述成資料隱藏或是封裝(encapsulation)。下表格就簡略彙整物件導向的一些專有名詞吧。

Definition Description
抽象類別(abstract class) 定義一組相關類別的行為
類別(class) 根據物件所具有的責任定義物件的類型。責任可以分為行為和狀態。這些分別是由分類和資料實作的。
具體類別(concrete class) 具體類別是一個特定的概念、不變的實作
封裝(encapsulation) 通常定義資料隱藏,但最好將它看作任何形式的隱藏
繼承(inheritance) 一個類別繼承另一個類別,並且接受該類別的一些或者所有性質。起始類別稱為基礎類別(base class)。而繼承類別稱為衍生類別(derived classes)。
實體(instance) 類別的特例(總是一個物件)。每個物件都有自己的狀態,因此同一個類別可以有多個物件。
介面(interface) 介面與類別類似,但是只為其成員提供規約而不提供實作。
多型(polymorphism) 用一種方式來參照一個類別的不同衍生類別,但獲得的行為對應於所參照的衍生類別。

範例:

inheritance.h
class Quote { 
    public:     
        Quote() = default;
        Quote(const std::string &book, double sales_price): bookNo(book), price(sales_price) { }
        std::string isbn() const { return bookNo; }     
                // returns the total sales price for the specified number of items     
                // derived classes will override and apply different discount algorithms     
        virtual double net_price(std::size_t n) const { return n * price; }     
        virtual ~Quote() = default; // dynamic binding for the destructor
        static void statmem();
    private:     
        std::string bookNo; // ISBN number of this item
    protected:     
        double price = 0.0; // normal, undiscounted price 
};
class Bulk_quote final : public Quote { // Bulk_quote inherits from Quote,
        // Bulk_quote can't be the base class     
        Bulk_quote() = default;     
        Bulk_quote(const std::string&, double, std::size_t, double);     
            // overrides the base version in order to implement the bulk purchase discount policy     
        double net_price(std::size_t) const override;
        soze_t count() { static size_t ctr = 0; return ++ctr;} // value will persist across calls
    private:     
        std::size_t min_qty = 0; // minimum purchase for the discount to apply     
        double discount = 0.0;   // fractional discount to apply 
};
dynamiclink.cc
// calculate and print the price for the given number of copies, applying any discounts 
double print_total(ostream &os, const Quote &item, size_t n) {
    // depending on the type of the object bound to the item parameter     
    // calls either Quote::net_price or Bulk_quote::net_price     
    double ret = item.net_price(n);     
    os << "ISBN: " << item.isbn() // calls Quote::isbn        
    << " # sold: " << n << " total due: " 
    << ret << endl;      
    return ret; 
}

<Remark>

  • 在C++中,一個基礎類別必須區分它預期其衍生類別會覆寫的函式(function),以及那些它預期衍生類別原封不動繼承的函式。
    • 預期其衍生類別會覆寫的函式(function),基礎類別會將它們定義為virtual。透過動態鏈結(dynamic binding)取決該參考或指標所鏈結的物件之型別,而該決策到執行時期(run time)才能決定。因此,動態鏈結也可稱作執行期鏈結(run-time binding)。 “ The base class defines as virtual those function it expects its derived classes to define for themselves. Through dynamic binging, we can use the same code to process objects interchangeably. The decision as to which version of objects to run depends on the type of the argument, that decision can’t be made until runtime. Therefore, dynamic binging is sometimes known as run-time binding. ”
    • 那些預期衍生類別原封不動繼承的函式,則是在編譯時期(compile time)解析的,而非執行時期(run time)。
  • C++11標準中,讓一個衍生類別中的一個virtual成員函式明確地指定覆寫override specifier。 “The new standard lets a derived class explicity note that it intends a member function to override a virtual that it inherits. It does so by specifying override after its oarameter list.”
  • C++11標準中,可以在類別名稱後加上final specifier來防止一個類別被當成基礎來用。
  • 基礎類別定義一個static member,不管多少類別衍生自一個基礎類別,每個static member都只會存在單一實體(instance),並且可透過基礎或衍生來使用static member。 “If a base class defines a static member, there is only one such member defined for the entire hierarchy. Regardless of the member of classes derived from a base class, there exists a single instance of each static member.”
  • 將一個區域變數(local variable)定義為static來獲得這種物件,每個區域static物件都會在執行第一次經過物件的定義時被初始化,並不會在函式結束時被摧毀,而是在程式終結時才被摧毀。
  • 區域變數或其他運算式可區分為靜態型別(static type)與該運算式所代表的物件之動態型別(dynamic type)。靜態型別(static type)在編譯時期(compile time)就已經知道所宣告的型別;動態型別(dynamic type)可能要到執行時期(run time)才能確定。

物件導向程式設計實踐的概念

使用物件導向方式來解決設計上的問題,可以試著定義一些物件和這些物件具有的責任。舉例:實踐形狀(shape)的實體必須完成以下任務:

  1. 在資料庫中找到形狀清單 - ShapeDataBase class
  2. 打開形狀清單 - Collection class
  3. 依某種規則將清單排序 - Square/Circle class
  4. 在螢幕上顯示各個形狀- Display

這範例所需要的物件(類別)如下:

物件(類別) 責任(方法)
ShapeDataBase class getCollection - 獲得指定形狀的集合
Shape(抽象類別) class display - 為形狀定義介面
getX - 返回形狀的x座標(用於排序)
getY - 返回形狀的y座標(用於排序)
Square(衍生自Shape類別) class dispay - 顯示正方形
Circle(衍生自Shape類別) class dispay - 顯示圓形
Collection class dispay - 顯示所存放的所有形狀
sort - 對形狀集合排序
Display draw - 在銀幕上畫一條線
drawCircle - 在螢幕上畫一圈

現在這範例的主程式執行步驟流程應該與下面給的類似:

  1. 主程式建立一個資料庫(ShapeDataBase)物件的實體
  2. 主程式要求資料庫(ShapeDataBase)物件找到一組形狀,然後實體化一個保存這些形狀(Circle以及Square物件)的Collection物件
  3. 主程式要求Collection物件將所存放的形狀排序
  4. 主程式要求Collection物件顯示形狀(Circle以及Square物件)
  5. Collection物件要求所存放的所有形狀(Circle以及Square物件)顯示自己
  6. 每個形狀(Circle以及Square物件)根據形狀種類顯示自己

這簡單封裝物件導向的範例,直接藴涵了以下幾項優點:

  • 使用更容易,因為使用者不需要再操心實作問題
  • 可以在不考慮呼叫者的情況下修改實作,因為呼叫者與實作不應該存在任何相依關係
  • 其他物件對該物件內部是未知,而這些外部物件往往用來幫助時做該物件接面所指定的功能
  • 有助於防止不良副作用
  • 物件對自己行為所負的責任越多,控制程式需要負的責任就越少

極限程式設計的概念

極限程式設計中提倡循序漸進地開發,在程式設計的同時進行驗證,要求程式碼具有可變性、無冗餘、可讀性、可測試性、高內聚以及鬆耦合。此外,要遵循的一個非常重要的策略就是,某個規則只在一個地方實作(Once and only once rule),並且在編寫程式碼之前就要撰寫測試(單元測試),以便最後能得到一組自動化測試,也有助於將概念分成多個可測試的部分,獲得強內聚及鬆耦合的程式碼。

建立物件導向程式模型的圖形語言 - UML

UML 是一種用來建立物件導向程式模型的圖形語言,可以說明程式碼中物件之間的關聯性(聚合、組合、繼承以及相依)。功能上可用於分析、觀察物件的交流、觀察物件所處的狀態不同時行為的差異或是了解程式如何部署。只要正確地使用,UML還是能良好促進交流的,即使在使用結對程式設計(paired programming)時,設計概念在概念層次描述通常也比在程式碼(即實作)層次描述更好。換句話說,應該努力同時做到儘可能最簡和儘可能最好。這邊值得一提的是Doxygen可支援自動建立C++程式碼或專案的UML圖形,其對應的設定選項可參考 How to use doxygen to create UML diagrams的說明。

Access specifiers Description
public(+) 所有物過都可以存取這個資料或方法
protected or friend(#) 只有該類別及其所有衍生類別可以存取這個資料或方法
private(-) 只有該類別的方法可以存取這個資料或方法

類別(class)的關係記號

一般性而言,程式系統非常複雜,有許多不同種類的資訊需要傳遞,所以UML提供許多不同的圖(關係記號)專門表示不同種類的資訊,如下:

UML relations and its notation:

  • Association(結合): It represents the static relationship shared among the objects of two classes
  • Inheritance(繼承): It indicates that one of the two related classes (the subclass) is considered to be a specialized form of the other (the supper type or bases type) and the superclass (the base class) is considered a Generalization of the subclass
  • Composition(組合): Its relationship is very similar to the aggregation relationship. with the only difference being its key purpose of emphasizing the dependence of the contained class to the life cycle of the container class.
  • Aggregation(聚合): It can occur when a class is a collection or container of other classes, but the contained classes do not have a strong lifecycle dependency. The contents of the container still exist when container is destoryed

這些類別(class)之間的關係存在is-a(是一種/一個)以及has-a (擁有一個)兩種類型(class)的關係。Inheritance(繼承)說明抽象類別可用來為其衍生類別定義介面而且存放這些衍生類別公共資料和方法的類別,因此可以表示is-a關係。Aggregation(聚合)以及Composition(組合)分別用來說明一個物件可以擁有另外一個物件以及被包含物件是包含物件的一部分,因此可以表示has-a關係。

Note that the differences between Composition and Aggregation:

Instance-level relationships relationship
Composition When the container is detoryed, the contents are also detoryed
Aggregation When the container is detoryed, the contents are usually not destoryed.

對於一名初級物件導向分析師而言,只用繼承實作似乎很正常,但是隨著時間推移將變得越來越難以維護。使用設計模式將有助於加快拯救這局面,其中就包括從繼承(inheritance)到組合(composition)的轉變,因此有經驗的物件導向分析師了解到應該有選擇地使用繼承,才能發揮其優勢。

=========== To be continued…. ==========

Reference

Thanks for reading! Feel free to leeve the comments below or email to me. Any pieces of advice or discussions are always welcome. :)