面向物件程式設計/繼承
在許多書籍中,繼承和 OOP 被認為是同義詞,因此我們如此遲才討論這個話題可能看起來很奇怪。這反映了繼承隨著時間的推移而發揮的作用越來越小。事實上,經典 OOP 和現代 OOP 之間的主要區別之一就在於繼承的使用方式。正如老話所說,如果你只有一把錘子,那麼所有東西看起來都像釘子。因此,繼承往往是當時 OOP 程式設計師唯一的可用工具,因此所有概念都被塞進了繼承之中。這種缺乏概念完整性和關注點分離導致了過於親密的依賴關係和許多困難。在一些語言中,程式設計師的技術發展使得使用相同的有限語言功能使概念更清晰,而其他語言則明確開發了功能來解決這些問題。由於你在 OOP 學習中的某個階段幾乎肯定會接觸到一些這些誤導性的建議,因此我們將嘗試解釋一些問題。然而,大部分討論將在它們適得其所的部分進行!
首先,什麼是繼承?嗯,就像你奶奶去世了你繼承了她那件難看透頂的燈一樣,OOP 中的繼承會賦予子類(或派生類)所有父類(或超類)的屬性。例如(在 C++ 中)
class Parent
{
public:
int f() {return 10;}
};
class Child : public Parent
{
// see, nothing here! But wait, we inherited the function f()!
};
Child c;
int result = c.f(); // huh? Oh, f() was inherited from the parent!
你會看到關於繼承的最常用也是最無價值的討論圍繞著“Is-A 與 Has-A”展開。例如,汽車是車輛,但它有一個方向盤。這些作者想要表達的意思是,你的汽車類應該繼承你的車輛類,並且有一個方向盤作為成員。你可能還會遇到形狀的例子,其中矩形是形狀。關鍵是,再次強調,抽象。目標是識別僅對車輛或形狀的抽象概念進行操作的操作,然後只編寫一次程式碼。然後,透過繼承的魔力,你可以將汽車或矩形或任何東西傳遞給通用程式碼,它將會起作用,因為派生類是父類的一切,“再加上更多”。
這裡的問題是,繼承將幾個東西混雜在一起:你同時繼承了“型別性”、“介面”和“實現”。然而,所有這些例子都關注介面,而談論的是“型別性”。抽象程式碼並不關心汽車“是”車輛,只關心物件是否響應一組特定的函式,即介面。事實上,如果你想給你的椅子類賦予 accelerate()、brake()、turn_left() 和 turn_right() 方法,抽象程式碼是否應該能夠處理椅子?當然,但那並不能使椅子成為車輛。
因此,在這些“is-a”討論中提出的解決方案,大多已被所謂的介面程式設計和模板程式設計所取代。由於模板程式設計提供了最鬆散的耦合,因此它將注意力集中到語法語義混淆上。僅僅因為你擁有具有正確名稱的函式,是否意味著它們按照你的預期工作?如果椅子類擁有 accelerate()、brake() 和其他車輛型別的函式,那麼讓通用車輛程式碼處理這個椅子是否合理?這導致了對通用程式碼假設的更多規範:例如,brake(INFINITY) ==> STOPPED。這意味著 brake(x) != accelerate(-x)。因此,椅子可能比企業號宇宙飛船更適合成為車輛!
你將在後面關於訊息傳遞的部分看到更多關於委託的內容,但我們要提到,在沒有提供委託功能的語言中,繼承通常解決了許多由委託解決的問題。舉個例子比解釋更容易,因此簡而言之,以下是委託的示例
interface I
{
int f();
}
class A implements I
{
// A member variable that actually services all requests for
// calls to the I interface.
delegate I private_i;
A(I target)
{
private_i = target;
}
}
class B implements I
{
int f() { return 1; }
}
B b; // create a B object to be the target of A's delegation
A a(b); // Create an A object, passing in b
int foo = a.f(); // This call to f() is delegated to b.f(), causing 1 to be returned.
繼承完成了一個類似的壯舉
class B
{
int f() { return 1; }
}
class A inherits B
{
}
A a;
int foo = a.f(); // returns 1 just like in the delegation example.
但是,委託可以隨著時間推移而改變,而基類則不能。因此,委託有時被稱為“動態繼承”。
多重繼承是指一個物件從多個不同的類繼承其屬性。例如,房子既是“建築物”,又是“睡覺的地方”。
在多重繼承中,一個物件從多個物件(其父類或基類)繼承其屬性。因此,我們需要設定一些類,我們將使用 C++ 來完成
class Building
{
int size;
int purpose;
int price;
};
class PlaceToSleepIn
{
int type; // tent, house, dorm, etc.
bool pleasant_to_live_in; // true or false
};
在 C++ 中,要繼承所有型別,使用 : 運算子,例如
class House : public Building, public PlaceToSleepIn
{
// all of the Building and PlaceToSleepIn variables are here!
int rooms; // the number of rooms
};
繼承的一個問題是,預定義類的使用者可能不知道抽象資料的儲存位置。因此,他們可能難以獲取它。大多數繼承問題是由於設計脆弱性或程式語言實現方式引起的。
在處理大型程式中的繼承時,一個常見的問題被稱為脆弱的基類。當基類的子類對基類提供的契約之外的屬性(屬性的定義)做出假設時,就會出現這種情況。
一個例子可能是,一個屬性在基類中宣告為位元組,但後來發現需要字的範圍。將其更改為字將至少需要重新編譯所有子類,但這可能還不夠,因為子類可能依賴於該屬性的型別,因此也需要更改原始碼。
這種關於給定實現的實現者固有知識,通常是通用程式設計中的問題,但 OOP/OOD 試圖透過提供更多機會來實現更好的程式設計實踐來解決這個問題。但與程式碼中的任何其他事物一樣,它取決於程式設計師是否充分利用它,使其儘可能透明,以便其他人能夠充分利用其程式碼,從而利用繼承的特性。