Ada 樣式指南/面向物件特性
本章推薦使用 Ada 的面向物件特性的方法。Ada 支援繼承和多型性,為程式設計師提供了一些有效技術和構建塊。這些特性的規範使用將促進易於閱讀和修改的程式。這些特性還為程式設計師提供了構建可重用元件的靈活性。
為了使本章更容易理解,提供了以下定義。面向物件程式設計的基本特徵是封裝、繼承和多型性。這些在理由 (1995, §§4.1 和 III.1.2) 中定義如下
- 繼承
- 一種透過“繼承”現有抽象的屬性來增量構建新抽象的方法,而不會影響原始抽象或現有客戶端的實現。
- 多重繼承
- 從兩個或多個父抽象繼承元件和操作的方法。
- 混合繼承
- 多重繼承,其中一個或多個父抽象不能有自己的例項,而只存在於為從它們繼承的抽象提供一組屬性。
- 多型性
- 一種將抽象集合中的差異提取出來的方法,使得程式可以根據共同的屬性編寫。
- 靜態多型性透過泛型引數機制提供,泛型單元可以在編譯時使用來自型別類的任何型別例項化。
- 動態多型性透過使用所謂的類範圍型別提供,然後根據標記的值在執行時進行區分(“實際上是一個隱藏的判別式,用於識別型別”[理由 1995, §II.1])。
如 Ada 語言參考手冊所述 (1995, 附錄 N [註釋])
- 型別具有相關聯的值集和實現其語義基本方面的基本操作集。
類是型別集,在派生方面是封閉的,這意味著如果給定型別在類中,則從該型別派生的所有型別也在該類中。類中的型別集共享共同的屬性,例如它們的基本操作。類的語義包括預期行為和例外情況。
物件是根據型別(類)定義的常量或變數。物件包含一個值。物件的子元件本身是物件。
本章中的指南經常以“考慮...”開頭,因為硬性規則不能應用於所有情況。您在特定情況下的具體選擇涉及設計權衡。這些指南的原理旨在讓您深入瞭解一些權衡。
如果您已經完成面向物件設計,您會發現更容易利用本章中的許多概念。面向物件設計的成果將包括一組有意義的抽象和類層次結構。抽象需要包括設計物件的定義,包括結構和狀態、對物件的運算以及每個物件的預期封裝。有關設計這些抽象和類層次結構的詳細資訊超出了本書的範圍。許多好的資料可以提供這些詳細資訊,包括 Rumbaugh 等人 (1991)、Jacobson 等人 (1992)、軟體生產力聯盟 (1993) 和 Booch (1994)。
設計過程中的一個重要部分是決定系統的整體組織。從單個型別、單個包,甚至單個型別類開始,可能不是一個好的起點。合適的起點應該更多地處於“子系統”或“框架”級別。您應該使用子包(指南 4.1.1 和 4.2.2)將抽象集分組到子系統中,這些子系統表示可重用的框架。您應該區分框架的“抽象”可重用核心和框架的特定“例項化”。假設框架構建正確,抽象核心及其例項化可以分離到包層次結構中不同的子系統中,因為抽象可重用框架的內部可能不需要對框架的特定例項化可見。
您應該主要將繼承用作從面向物件設計實現類層次結構的機制。類層次結構應該是泛化/特化(“是-a”)關係。這種關係也稱為“是-a-kind-of”,不要與“是-an-instance-of”混淆。這種“是-a”的繼承使用方式與其他語言中使用繼承也提供 Ada 上下文子句 with 和 use 等效的功能的方式形成對比。在 Ada 中,您首先透過 with 子句標識感興趣的外部模組,然後選擇性地選擇是否僅使模組(包)的名稱可見或其內容(透過 use 子句)可見。
- 在設計 is-a(泛化/特化)層次結構時,請考慮使用型別擴充套件。
- 使用標記型別來保留跨不同實現的通用介面(Taft 1995a)。
- 在包中定義帶標記型別時,請考慮包含對相應類寬型別的一般訪問型別的定義。
- 通常,每個包只定義一個帶標記型別。
示例
[edit | edit source]考慮一組位於笛卡爾座標系中的二維幾何物件的型別結構(Barnes 1996)。祖先或根型別 Object 是一個帶標記記錄。此型別及其所有後代共有的元件是 x 和 y 座標。各種後代型別包括點、圓和任意形狀。除點外,這些後代型別透過新增其他元件擴充套件根型別;例如,圓添加了一個半徑元件。
type Object is tagged
record
X_Coord : Float;
Y_Coord : Float;
end record;
type Circle is new Object with
record
Radius : Float;
end record;
type Point is new Object with null record;
type Shape is new Object with
record
-- other components
...
end record;
以下是一般訪問型別對應類寬型別的示例。
package Employee is
type Object is tagged limited private;
type Reference is access all Object'class;
...
private
...
end Employee;
理由
[edit | edit source]您可以從帶標記型別和無標記型別派生新型別,但這兩種派生的效果不同。從無標記型別派生時,您正在建立一個新的型別,其實現與父型別相同。派生型別的數值受強型別檢查約束;因此,您不能混用比喻中的蘋果和橙子。從無標記型別派生新型別時,不允許您使用新元件對其進行擴充套件。您實際上是在建立新的介面,而不會更改底層實現(Taft 1995a)。
從帶標記型別派生時,您可以使用新元件擴充套件型別。每個後代都可以擴充套件通用介面(父型別的介面)。帶標記型別及其後代的並集形成一個類,而類提供了一些無標記派生所沒有的獨特功能。您可以編寫適用於類中任何物件的類寬操作。您還可以為帶標記型別的後代提供新的實現,方法是覆蓋繼承的原始操作或建立新的原始操作。最後,帶標記型別可用作多重繼承構建塊的基礎(參見準則 9.5.1)。
引用語義在面向物件程式設計中非常常用。特別是基於帶標記型別的異構多型資料結構需要使用訪問型別。為定義帶標記型別的包的任何客戶端提供這種型別的通用定義非常方便。異構多型資料結構是一種複合資料結構(例如陣列),其元素具有同構介面(即對類寬型別的訪問)且元素的實現是異構的(即元素的實現使用不同的特定型別)。另請參見關於多型性的準則 9.3.5 和關於管理帶標記型別層次結構的可見性的準則 9.4.1。
在 Ada 中,型別的原始操作透過作用域規則隱式地與該型別相關聯。帶標記型別的定義和一組操作一起對應於面向物件程式設計中“類”的“傳統”概念。將它們放入包中提供了一種乾淨的封裝機制。
例外情況
[edit | edit source]如果層次結構的根未定義一組完整的數值和操作,則使用抽象帶標記型別(參見準則 9.2.4)。這種抽象型別可以被認為是類的最小公分母,本質上是一種概念性和不完整的型別。
如果後代需要刪除其祖先的一個元件或原始操作,則擴充套件帶標記型別可能不合適。
使用引用語義的一個例外是,當匯出一個不會在資料結構中使用或不會成為集合一部分的型別時。
如果兩個帶標記型別的實現需要相互可見性,並且這兩個型別通常一起使用,那麼最好在同一個包中一起定義它們,儘管應該考慮使用子包(參見準則 9.4.1)。此外,在同一個包規範中定義一個小型的(完全)抽象型別層次結構(或大型層次結構的一部分)也可能很方便;但是,對可維護性的負面影響可能會超過便利性。除非您已在層次結構成員上聲明瞭非抽象操作,否則在這種情況下不會提供包體。
排程操作的屬性
[edit | edit source]指南
[edit | edit source]- 以帶標記型別 T 為根的派生類中每個型別的排程操作的實現應符合對應類寬型別 T'Class 的排程操作的預期語義。
示例
[edit | edit source]以下示例中兩種備選方案的關鍵點是,必須能夠多型地使用類寬型別 Transaction.Object'Class,而不必研究從根型別 Transaction.Object 派生的每個型別的實現。此外,可以在不使現有的事務處理程式碼失效的情況下,將新的事務新增到派生類中。這些是準則中捕獲的設計規則的重要實際後果。
with Database;
package Transaction is
type Object (Data : access Database.Object'Class) is abstract tagged limited
record
Has_Executed : Boolean := False;
end record;
function Is_Valid (T : Object) return Boolean;
-- checks that Has_Executed is False
procedure Execute (T : in out Object);
-- sets Has_Executed to True
Is_Not_Valid : exception;
end Transaction;
Execute(T) 的前提條件對於 Transaction.Object'Class 中的所有 T 來說是 Is_Valid(T) 為 True。後置條件是 T.Has_Executed = True。此模型由根型別 Transaction.Object 輕鬆滿足。
考慮以下派生型別。
with Transaction;
with Personnel;
package Pay_Transaction is
type Object is new Transaction.Object with
record
Employee : Personnel.Name;
Hours_Worked : Personnel.Time;
end record;
function Is_Valid (T : Object) return Boolean;
-- checks that Employee is a valid name, Hours_Worked is a valid
-- amount of work time and Has_Executed = False
procedure Has_Executed (T : in out Object);
-- computes the pay earned by the Employee for the given Hours_Worked
-- and updates this in the database T.Data, then sets Has_Executed to True
end Pay_Transaction;
特定操作 Pay_Transaction.Execute(T) 的前提條件是 Pay_Transaction.Is_Valid(T) 為 True,這與類寬型別上排程操作 Execute 的前提條件相同。(實際的有效性檢查不同,但“前提條件”的表述相同。)Pay_Transaction.Execute(T) 的後置條件包括 T.Has_Executed = True,但也包括 T.Data 上用於計算工資的適當條件。
然後可以如下使用類寬事務型別。
type Transaction_Reference is access all Transaction.Object'Class;
type Transaction_List is array (Positive range <>) of Transaction_Reference;
procedure Process (Action : in Transaction_List) is
begin
for I in Action'Range loop
-- Note that calls to Is_Valid and Execute are dispatching
if Transaction.Is_Valid(Action(I).all) then
-- the precondition for Execute is satisfied
Transaction.Execute(Action(I).all);
-- the postcondition Action(I).Has_Executed = True is
-- guaranteed to be satisfied (as well as any stronger conditions
-- depending on the specific value of Action(I))
else
-- deal with the error
...
end if;
end loop;
end Process;
如果您未在事務上定義操作 Is_Valid,則工資計算的有效性條件(有效的姓名和工作時間)將必須直接成為 Pay_Transaction.Execute 的前提條件。但這將比類寬排程操作的“更強”的前提條件,違反了該準則。由於違反了此準則,因此無法保證對 Execute 的排程呼叫的前提條件,從而導致意外錯誤。
解決此問題的另一種方法是定義一個異常,當事務無效時由 Execute 操作引發。此行為成為類寬型別的語義模型的一部分:Execute(T) 的前提條件變為僅僅是 True(即始終有效),但後置條件變為“要麼”異常未引發且 Has_Executed = True“要麼”異常引發且 Has_Executed = False。然後,所有派生事務型別中 Execute 的實現都需要滿足新的後置條件。重要的是,“所有”實現都應引發“相同的”異常,因為這是類寬型別的預期語義模型的一部分。
使用替代方法,上面的處理迴圈變為
procedure Process (Action : in Transaction_List) is
begin
for I in Action'Range loop
Process_A_Transaction:
begin
-- there is no precondition for Execute
Transaction.Execute (Action(I).all);
-- since no exception was raised, the postcondition
-- Action(I).Has_Executed = True is guaranteed (as well as
-- any stronger condition depending on the specific value of
-- Action(I))
exception
when Transaction.Is_Not_Valid =>
-- the exception was raised, so Action(I).Has_Executed = False
-- deal with the error
...
end Process_A_Transaction;
end loop;
end Process;
理由
[edit | edit source]該型別客戶端對類寬型別的所有預期屬性都應對類寬型別派生類中的任何特定型別都有意義。此規則與面向物件程式設計中關於面向物件超類及其子類之間的語義一致性的“可替代性原則”相關(Wegner and Zdonik 1988)。但是,在 Ada 95 中,多型類寬型別 T'Class 與根特定型別 T 的分離將此原則闡明為派生類上的設計規則,而不是派生本身的正確性原則。
當在類寬型別 T'Class 的變數上使用排程操作時,執行的實際實現將動態地取決於變數中值的實際標記。為了合理地使用 T'Class,必須能夠理解 T'Class 上操作的語義,而不必研究以 T 為根的派生類中每個型別的操作的實現。此外,新增到此派生類中的新型別不應使 T'Class 的這種整體理解失效,因為這可能會使類寬型別的現有使用失效。因此,需要 T'Class 操作的一組整體語義屬性,這些屬性由以 T 為根的派生類中所有型別的相應排程操作的實現保留。
捕獲操作語義屬性的一種方法是定義一個“前提條件”,該條件必須在呼叫操作之前為真,以及一個“後置條件”,該條件必須在操作執行後(在前提條件成立的情況下)為真。您可以(正式或非正式地)為 T'Class 的每個操作定義前置條件和後置條件,而無需參考特定型別的排程操作的實現。這些語義屬性定義了派生類中所有型別共有的“最小”屬性集。為了保留此最小屬性集,以 T 為根的派生類中所有型別的排程操作的實現(包括根型別 T)應具有(相同或)比 T'Class 的對應操作更弱的前提條件以及(相同或)比 T'Class 操作更強的後置條件。這意味著對 T'Class 的任何排程操作呼叫都將導致執行一個實現,該實現的要求不超過對排程操作的一般期望(儘管它可能要求更少),並且會提供一個不低於期望的結果(儘管它可能做得更多)。
例外情況
[edit | edit source]標記型別和型別擴充套件有時主要用於型別實現原因,而不是用於多型性和排程。 特別是,非標記私有型別可以使用標記型別的型別擴充套件來實現。 在這種情況下,派生型別的實現可能不需要保留類寬型別的語義屬性,因為新型別在標記型別派生類中的成員資格通常不會為型別的客戶端所知。
- 當型別分配必須在銷燬或覆蓋時釋放或以其他方式“清理”的資源時,請考慮使用受控型別。
- 優先使用從受控型別派生,而不是提供必須由型別客戶端呼叫的顯式“清理”操作。
- 當覆蓋從受控型別派生的調整和終結過程時,請定義終結過程以撤消調整過程的影響。
- 派生型別初始化過程應在型別特定初始化的一部分中呼叫其父級的初始化過程。
- 派生型別終結過程應在其型別特定終結的一部分中呼叫其父級的終結過程。
- 請考慮從受控型別派生資料結構的元件,而不是從受控型別派生封閉資料結構。
以下示例演示了在簡單鏈表實現中使用受控型別。 因為 Linked_List 型別派生自 Ada.Finalization.Controlled,所以當 Linked_List 型別的物件完成其執行範圍時,將自動呼叫 Finalize 過程。
with Ada.Finalization;
package Linked_List_Package is
type Iterator is private;
type Data_Type is ...
type Linked_List is new Ada.Finalization.Controlled with private;
function Head (List : Linked_List) return Iterator;
procedure Get_Next (Element : in out Iterator;
Data : out Data_Type);
procedure Add (List : in out Linked_List;
New_Data : in Data_Type);
procedure Finalize (List : in out Linked_List); -- reset Linked_List structure
-- Initialize and Adjust are left to the default implementation.
private
type Node;
type Node_Ptr is access Node;
type Node is
record
Data : Data_Type;
Next : Node_Ptr;
end record;
type Iterator is new Node_Ptr;
type Linked_List is new Ada.Finalization.Controlled with
record
Number_Of_Items : Natural := 0;
Root : Node_Ptr;
end record;
end Linked_List_Package;
--------------------------------------------------------------------------
package body Linked_List_Package is
function Head (List : Linked_List) return Iterator is
Head_Node_Ptr : Iterator;
begin
Head_Node_Ptr := Iterator (List.Root);
return Head_Node_Ptr; -- Return the head element of the list
end Head;
procedure Get_Next (Element : in out Iterator;
Data : out Data_Type) is
begin
--
-- Given an element, return the next element (or null)
--
end Get_Next;
procedure Add (List : in out Linked_List;
New_Data : in Data_Type) is
begin
--
-- Add a new element to the head of the list
--
end Add;
procedure Finalize (List : in out Linked_List) is
begin
-- Release all storage used by the linked list
-- and reinitialize.
end Finalize;
end Linked_List_Package;
三個控制操作:Initialize、Adjust 和 Finalize 充當自動呼叫的過程,這些過程控制物件生命週期中的三個基本活動(Ada 參考手冊 1995,第 7.6 節 [帶註釋的])。 當對派生自 Controlled 的型別的物件的賦值發生時,調整和終結會協同工作。 終結清理被覆蓋的物件(例如,回收堆空間),然後調整在已複製要分配的值後完成分配工作(例如,實現深複製)。
您可以透過從派生型別的初始化中呼叫父型別的初始化來確保派生型別的初始化與父型別的初始化一致。
您可以透過從派生型別的終結中呼叫父型別的終結來確保派生型別的終結與父型別的終結一致。
通常,您應該在子代特定初始化之前呼叫父初始化。 同樣,您應該在子代特定終結之後呼叫父終結。(您可以將父初始化和/或終結放在過程的開頭或結尾。)
- 請考慮在建立分類方案(例如,分類法)時使用抽象型別和操作,其中只有葉物件在應用程式中具有意義。
- 請考慮將型別樹中的根型別和內部節點宣告為抽象。
- 請考慮將抽象型別用於泛型形式派生型別。
- 請考慮使用抽象型別來開發單個抽象的不同實現。
在銀行應用程式中,有各種各樣的賬戶型別,每種型別都有不同的功能和限制。 一些變化包括費用、透支保護、最低餘額、允許的賬戶關聯(例如,支票和儲蓄)以及開戶規則。 所有銀行賬戶共有的都是所有權屬性:唯一的賬戶號碼、所有者姓名和所有者稅號。 所有賬戶型別的常見操作包括開戶、存款、取款、提供當前餘額和關閉賬戶。 這些共同的屬性和操作描述了概念上的銀行賬戶。 這種理想化的銀行賬戶可以形成一個泛化/專門化層次結構的根,該層次結構描述了銀行的產品陣列。 透過使用抽象標記型別,您可以確保只建立對應於特定產品的賬戶物件。 由於任何抽象操作都必須在每個派生中被覆蓋,因此您可以確保為專門的賬戶實施任何限制(例如,如何以及何時應用賬戶特定的費用結構)
--------------------------------------------------------------------------
package Bank_Account_Package is
type Bank_Account_Type is abstract tagged limited private;
type Money is delta 0.01 digits 15;
-- The following abstract operations must be overridden for
-- each derivation, thus ensuring that any restrictions
-- for specialized accounts will be implemented.
procedure Open (Account : in out Bank_Account_Type) is abstract;
procedure Close (Account : in out Bank_Account_Type) is abstract;
procedure Deposit (Account : in out Bank_Account_Type;
Amount : in Money) is abstract;
procedure Withdraw (Account : in out Bank_Account_Type;
Amount : in Money) is abstract;
function Balance (Account : Bank_Account_Type)
return Money is abstract;
private
type Account_Number_Type is ...
type Account_Owner_Type is ...
type Tax_ID_Number_Type is ...
type Bank_Account_Type is abstract tagged limited
record
Account_Number : Account_Number_Type;
Account_Owner : Account_Owner_Type;
Tax_ID_Number : Tax_ID_Number_Type;
end record;
end Bank_Account_Package;
--------------------------------------------------------------------------
-- Now, other specialized accounts such as a savings account can
-- be derived from Bank_Account_Type as in the following example.
-- Note that abstract types are still used to ensure that only
-- account objects corresponding to specific products will be
-- created.with Bank_Account_Package;
with Bank_Account_Package;
package Savings_Account_Package is
type Savings_Account_Type is abstract
new Bank_Account_Package.Bank_Account_Type with private;
-- We must override the abstract operations provided
-- by Bank_Account_Package. Since we are still declaring
-- these operations to be abstract, they must also be
-- overridden by the specializations of Savings_Account_Type.
procedure Open (Account : in out Savings_Account_Type) is abstract;
procedure Close (Account : in out Savings_Account_Type) is abstract;
procedure Deposit (Account : in out Savings_Account_Type;
Amount : in Bank_Account_Package.Money) is abstract;
procedure Withdraw (Account : in out Savings_Account_Type;
Amount : in Bank_Account_Package.Money) is abstract;
function Balance (Account : Savings_Account_Type)
return Bank_Account_Package.Money is abstract;
private
type Savings_Account_Type is abstract
new Bank_Account_Package.Bank_Account_Type with
record
Minimum_Balance : Bank_Account_Package.Money;
end record;
end Savings_Account_Package;
--------------------------------------------------------------------------
請參閱指南 9.5.1 中的抽象集包,該示例演示了使用單個介面和多個潛在實現建立抽象。 該示例只顯示了一種可能的實現; 但是,您可以使用其他資料結構提供 Hashed_Set 抽象的另一種實現。
在許多分類方案中,例如分類法,只有分類樹葉上的物件在應用程式中具有意義。 換句話說,層次結構的根沒有定義應用程式可使用的完整的值集和操作集。 使用“抽象”保證不會存在根或中間節點的物件。 需要抽象型別的具體派生和子程式,以便樹的葉子成為客戶端可以操作的物件。
只有當根型別也是抽象型別時,您才能宣告抽象子程式。 當您構建一個形成抽象系列基礎的抽象時,這很有用。 透過將原始子程式宣告為抽象,您可以編寫“系統的通用類寬部分……而根本不依賴於任何特定型別的屬性”(原理 1995,第 4.2 節)。
抽象型別和操作可以幫助您解決標記型別層次結構違反類寬型別排程操作的預期語義時遇到的問題。 原理(1995,第 4.2 節)解釋說
- 當構建一個要作為一類型別的基礎的抽象時,通常不為根型別提供實際的子程式,而只是提供抽象子程式,這些子程式可以在繼承時被替換。 只有當根型別宣告為抽象型別時才允許這樣做; 抽象型別的物件不能存在。 這種技術使系統中通用的類寬部分能夠在根本不依賴於任何特定型別的屬性的情況下編寫。 排程始終有效,因為已知永遠不會存在任何抽象型別物件,因此永遠不會呼叫抽象子程式。
請參閱指南 8.3.8 和 9.2.1。
指南 9.5.1 中討論的多重繼承技術利用了抽象標記型別。 基本抽象是使用帶有抽象原始操作的小型集的抽象標記(受限)私有型別(其完整型別宣告為空記錄)來定義的。 雖然抽象操作沒有主體,因此不能被呼叫,但它們會被繼承。 抽象的派生然後使用提供資料表示的元件擴充套件根型別,並覆蓋抽象操作以提供可呼叫的實現(原理 1995,第 4.4.3 節)。 這種技術允許您構建單個抽象的多個實現。 您宣告一個介面,並更改資料表示和操作實現的細節。
當您按照本指南中描述的那樣使用抽象資料型別時,您可以在單個程式中使用同一抽象的多個實現。 這種技術與編寫多個包體以提供在包規範中定義的抽象的不同實現的想法不同,因為使用包體技術,您只能在程式中包含一個實現(即,主體)。
在定義標記型別及其後代的操作時,可以使用三種選項。這些類別是原始抽象、原始非抽象和類範圍操作。抽象操作必須為非抽象派生型別覆蓋。非抽象操作可以在子類中重新定義。類範圍操作不能被子類定義覆蓋。類範圍操作可以為派生型別中根植的派生類重新定義;但是,這種做法不鼓勵,因為它會在程式碼中引入歧義。透過仔細使用這些選項,可以確保抽象保留類範圍屬性,如指南 9.2.1 中所述。如上所述,此原則要求任何明顯派生自某些父型別的型別都必須完全支援父型別的語義。
原始操作和重新排程
[edit | edit source]指南
[edit | edit source]- 考慮基於沒有有意義的“預設”行為來宣告一個原始抽象操作。
- 考慮基於存在有意義的“預設”行為來宣告一個原始非抽象操作。
- 覆蓋操作時,覆蓋子程式不應引發被覆蓋子程式使用者不瞭解的異常。
- 如果在型別的操作實現中使用重新排程,且其特定意圖是讓某些重新排程到的操作被派生型別的專用化覆蓋,那麼在規範中作為父型別與其派生型別的“介面”的一部分清楚地記錄此意圖。
- 當在標記型別的原始操作的實現中使用重新排程(出於任何原因)時,在操作子程式的正文中記錄(以某種專案一致的方式)此使用情況,以便在維護期間能夠輕鬆找到它。
示例
[edit | edit source]此示例(Volan 1994)旨在展示從矩形乾淨地派生正方形。你不希望從矩形派生正方形,因為矩形的語義不適合正方形。(例如,你可以製作一個具有任意高度和寬度的矩形,但你不應該能夠以這種方式製作正方形。)相反,正方形和矩形都應該從某種常見的抽象型別派生,例如
Any_Rectangle:
type Figure is abstract tagged
record
...
end record;
type Any_Rectangle is abstract new Figure with private;
-- No Make function for this; it's abstract.
function Area (R: Any_Rectangle) return Float;
-- Overrides abstract Area function inherited from Figure.
-- Computes area as Width(R) * Height(R), which it will
-- invoke via dispatching calls.
function Width (R: Any_Rectangle) return Float is abstract;
function Height (R: Any_Rectangle) return Float is abstract;
type Rectangle is new Any_Rectangle with private;
function Make_Rectangle (Width, Height: Float) return Rectangle;
function Width (R: Rectangle) return Float;
function Height (R: Rectangle) return Float;
-- Area for Rectangle inherited from Any_Rectangle
type Square is new Any_Rectangle with private;
function Make_Square (Side_Length: Float) return Square;
function Side_Length (S: Square) return Float;
function Width (S: Square) return Float;
function Height (S: Square) return Float;
-- Area for Square inherited from Any_Rectangle
...
-- In the body, you could just implement Width and Height for
-- Square as renamings of Side_Length:
function Width (S: Square) return Float renames Side_Length;
function Height (S: Square) return Float renames Side_Length;
function Area (R: Any_Rectangle) return Float is
begin
return Width(Any_Rectangle'Class(R)) * Height(Any_Rectangle'Class(R));
-- Casting [sic, i.e., converting] to the class-wide type causes the function calls to
-- dynamically dispatch on the 'Tag of R.
-- [sic, i.e., redispatch on the tag of R.]
end Area;
Alternatively, you could just wait until defining types Rectangle and Square to provide actual Area functions:
type Any_Rectangle is abstract new Figure with private;
-- Inherits abstract Area function from Figure,
-- but that's okay, Any_Rectangle is abstract too.
function Width (R: Any_Rectangle) return Float is abstract;
function Height (R: Any_Rectangle) return Float is abstract;
type Rectangle is new Any_Rectangle with private;
function Make_Rectangle (Width, Height: Float) return Rectangle;
function Width (R: Rectangle) return Float;
function Height (R: Rectangle) return Float;
function Area (R: Rectangle) return Float; -- Overrides Area from Figure
type Square is new Any_Rectangle with private;
function Make_Square (Side_Length: Float) return Square;
function Side_Length (S: Square) return Float;
function Width (S: Square) return Float;
function Height (S: Square) return Float;
function Area (S: Square) return Float; -- Overrides Area from Figure
...
function Area (R: Rectangle) return Float is
begin
return Width(R) * Height(R); -- Non-dispatching calls
end Area;
function Area (S: Square) return Float is
begin
return Side_Length(S) ** 2;
end Area;
理由
[edit | edit source]非抽象操作的行為可以解釋為該類所有成員的預期行為;因此,該行為必須對所有後代而言是一個有意義的預設值。如果操作必須根據後代抽象進行定製(例如,計算幾何形狀的面積取決於特定形狀),則該操作應該是原始的,可能是抽象的。將操作設為抽象的效果是,它保證每個後代都必須定義自己的操作版本。因此,當沒有可接受的基本行為時,抽象操作是合適的,因為每個派生都需要提供操作的新版本。
在與標記型別相同的包中宣告的所有操作,以及在標記型別宣告之後但下一個型別宣告之前的操作,都被認為是其原始操作。因此,當從標記型別派生新型別時,它將繼承原始操作。如果您不希望繼承任何操作,則必須選擇將其宣告為類範圍操作(請參閱指南 9.3.2)還是在單獨的包中宣告它們(例如,子包)。
異常是類語義的一部分。透過修改異常,你違反了類範圍型別的語義屬性(請參閱指南 9.2.1)。
標記型別及其原語(至少)有兩個不同的使用者。“普通”使用者使用該型別及其原語,無需增強。而“擴充套件”使用者透過基於現有(標記)型別派生型別來擴充套件該型別。擴充套件使用者和維護人員必須確定可能不正確的擴充套件的連鎖反應。本指南試圖在過多的文件(這很容易與實際程式碼不同步)和適當級別的文件之間取得平衡,以增強程式碼的可維護性。
與繼承和動態繫結相關的重大維護難題之一是,標記型別的原始(排程)操作之間未記錄的相互依賴關係(在典型的面向物件術語中相當於“方法”)。如果派生型別繼承了一些操作並覆蓋了其他原始操作,則會產生繼承的原語上的間接影響問題。如果未使用重新排程,則原語可以作為“黑盒”繼承。如果內部使用重新排程,則繼承時,操作的外部可見行為可能會發生變化,具體取決於覆蓋了哪些其他原語。當有人(故意或意外)覆蓋了重新排程中使用的操作時,就會出現維護問題(這裡指的是查詢和修復錯誤)。由於此覆蓋可能使從不正確操作向上繼承了多個級別的另一個操作的運作失效,因此追蹤起來可能非常困難。
在面向物件正規化中,重新排程通常用於引數化抽象。換句話說,某些原語的目的是被覆蓋,正是因為它們是重新排程。這些原語甚至可以被宣告為抽象的,要求它們被覆蓋。由於它們是重新排程,因此它們充當其他操作的“引數”。雖然在 Ada 中,大部分這種引數化可以透過泛型完成,但在某些情況下,重新排程方法可以帶來更清晰的面向物件設計。當你記錄要覆蓋的操作與使用它們的操作之間的重新排程連線時,你使型別的預期用法更加清晰。
因此,任何在原語中使用重新排程都應被視為原語的“介面”的一部分,至少對任何繼承者而言是如此,並且需要在規範級別進行記錄。另一種選擇(即不在規範中提供此類文件)是,必須深入研究派生層次結構中所有類的程式碼,以便繪製重新排程呼叫圖。這種偵察工作破壞了面向物件類定義的黑盒性質。請注意,如果你遵循指南 9.2.1 關於在派生型別的擴充套件中保留類範圍排程操作的語義,你將最大限度地減少或避免此處討論的有關重新排程的問題。
類範圍操作
[edit | edit source]指南
[edit | edit source]- 當可以在不知道給定標記型別的所有可能後代的情況下編寫、編譯和測試操作時,考慮使用類範圍操作(即具有類範圍型別引數的操作)(Barnes 1996)。
- 當你不希望操作被繼承和/或覆蓋時,考慮使用類範圍操作。
示例
[edit | edit source]以下示例改編自 Barnes(1996),使用指南 9.2.1 示例中的幾何物件,並將以下函式宣告為包規範中的原語
function Area (O : in Object) return Float;
function Area (C : in Circle) return Float;
function Area (S : in Shape) return Float;
現在可以使用類範圍型別建立用於計算力矩的函式,如下所示
function Moment (OC : Object'Class) return Float is
begin
return OC.X_Coord*Area(OC);
end Moment;
由於 Moment 接受 Object'Class 的類範圍形式引數,因此可以使用任何型別為 Object 的派生作為實際引數來呼叫它。假設所有型別為 Object 的派生都已定義用於 Area 的函式,那麼 Moment 在被呼叫時將排程到相應的函式。例如
C : Circle;
M : Float;
...
-- Moment will dispatch to the Area function for the Circle type.
M := Moment(C);
理由
[edit | edit source]使用類範圍操作避免了不必要的程式碼重複。可以在必要時使用執行時排程,根據運算元的標記呼叫適當的型別特定操作。
另請參閱指南 8.4.3,瞭解面向物件程式設計框架登錄檔中類範圍指標的討論。
建構函式
[edit | edit source]Ada 沒有為建構函式定義唯一的語法。在 Ada 中,型別的建構函式被定義為將構造物件(即型別的已初始化例項)作為結果生成的操作。
指南
[edit | edit source]- 避免將建構函式宣告為原始抽象操作。
- 僅當繼承的派生型別物件不需要額外的引數進行初始化時,才使用原始抽象操作宣告初始化函式或建構函式。
- 考慮使用訪問鑑別式來提供預設初始化的引數。
- 使用建構函式進行顯式初始化。
- 考慮將物件的初始化和構造分開。
- 考慮在子包中宣告建構函式操作。
- 考慮宣告一個建構函式操作以返回對已構造物件的訪問值(Dewar 1995)。
以下示例說明了在子包中宣告建構函式。
--------------------------------------------------------------------------
package Game is
type Game_Piece is tagged ...
...
end Game;
--------------------------------------------------------------------------
package Game.Constructors is
function Make_Piece return Game_Piece;
...
end Game.Constructors;
--------------------------------------------------------------------------
以下示例展示瞭如何將物件的初始化和構造分開。
type Vehicle is tagged ...
procedure Initialize (Self : in out Vehicle;
Make : in String);
...
type Car is new Vehicle with ... ;
type Car_Ptr is access all Car'Class;
...
procedure Initialize (Self : in out Car_Ptr;
Make : in String;
Model : in String) is
begin -- Initialize
Initialize (Vehicle (Self.all), Make);
...
-- initialization of Car
end Initialize;
function Create (Make : in String;
Model : in String) return Car_Ptr is
Temp_Ptr : Car_Ptr;
begin -- Create
Temp_Ptr := new Car;
Initialize (Temp_Ptr, Make, Model);
return Temp_Ptr;
end Create;
型別層次結構中型別的建構函式操作(假設標記型別及其派生型別)通常在引數配置檔案方面有所不同。建構函式通常需要更多引數,因為派生型別中添加了元件。當您讓建構函式操作被繼承時,您會遇到一個問題,因為您現在有了沒有意義的實現(預設或覆蓋)的操作。實際上,您違反了類範圍屬性(請參閱指南 9.2.1),因為根建構函式將無法成功構造派生物件。繼承的操作不能在其引數配置檔案中新增引數,因此這些操作不適合用作建構函式。
您無法在宣告時初始化受限型別,因此您可能需要使用訪問辨別式並依賴於預設初始化。但是,對於標記型別,您不應該假設任何預設初始化都足夠,並且您應該宣告建構函式。對於受限型別,建構函式必須是單獨的過程或函式,它們返回對受限型別的訪問。
該示例展示了在子包中使用建構函式。透過在子包或巢狀包中宣告建構函式操作,您可以避免與將其作為基本操作相關的問題。因為它們不再是基本操作,所以它們不能被繼承。透過在子包中宣告它們(另請參閱關於使用子包與巢狀包的指南 4.1.6 和 4.2.2),您獲得了在不影響父包客戶的情況下更改它們的能力(Taft 1995b)。
您應該將構造邏輯和初始化邏輯放在不同的子程式中,以便您可以呼叫父標記型別的初始化例程。
當您擴充套件標記型別(無論它是否為抽象型別)時,您可以選擇將一些附加操作宣告為抽象。但是,這樣做意味著派生型別也必須宣告為抽象。如果這個新派生的型別繼承了任何以其作為返回型別命名的函式,那麼這些繼承的函式現在也成為抽象函式(Barnes 1996)。如果這些基本函式之一用作建構函式函式,那麼您現在違反了第一個指南,因為建構函式已成為基本抽象操作。
- 當您在標記型別上重新定義 "=" 運算子時,請確保它在該型別的擴充套件中具有預期的行為,並在必要時覆蓋它。
以下示例改編自 Barnes(1996)中關於相等性和繼承的討論。
----------------------------------------------------------------------------
package Object_Package is
Epsilon : constant Float := 0.01;
type Object is tagged
record
X_Coordinate : Float;
Y_Coordinate : Float;
end record;
function "=" (A, B : Object) return Boolean;
end Object_Package;
----------------------------------------------------------------------------
package body Object_Package is
-- redefine equality to be when two objects are located within a delta
-- of the same point
function "=" (A, B : Object) return Boolean is
begin
return (A.X_Coordinate - B.X_Coordinate) ** 2
+ (A.Y_Coordinate - B.Y_Coordinate) ** 2 < Epsilon**2;
end "=";
end Object_Package;
----------------------------------------------------------------------------
with Object_Package; use Object_Package;
package Circle_Package_1 is
type Circle is new Object with
record
Radius : Float;
end record;
function "=" (A, B : Circle) return Boolean;
end Circle_Package_1;
----------------------------------------------------------------------------
package body Circle_Package_1 is
-- Equality is overridden, otherwise two circles must have exactly
-- equal radii to be considered equal.
function "=" (A, B : Circle) return Boolean is
begin
return (Object(A) = Object(B)) and
(abs (A.Radius - B.Radius) < Epsilon);
end "=";
end Circle_Package_1;
----------------------------------------------------------------------------
with Object_Package; use Object_Package;
package Circle_Package_2 is
type Circle is new Object with
record
Radius : Float;
end record;
-- don't override equality in this package
end Circle_Package_2;
----------------------------------------------------------------------------
with Object_Package;
with Circle_Package_1;
with Circle_Package_2;
with Ada.Text_IO;
procedure Equality_Test is
use type Object_Package.Object;
use type Circle_Package_1.Circle;
use type Circle_Package_2.Circle;
Object_1 : Object_Package.Object;
Object_2 : Object_Package.Object;
Circle_1 : Circle_Package_1.Circle;
Circle_2 : Circle_Package_1.Circle;
Circle_3 : Circle_Package_2.Circle;
Circle_4 : Circle_Package_2.Circle;
begin
Object_1 := (X_Coordinate => 1.000, Y_Coordinate => 2.000);
Object_2 := (X_Coordinate => 1.005, Y_Coordinate => 2.000);
-- These Objects are considered equal. Equality has been redefined to be
-- when two objects are located within a delta of the same point.
if Object_1 = Object_2 then
Ada.Text_IO.Put_Line ("Objects equal.");
else
Ada.Text_IO.Put_Line ("Objects not equal.");
end if;
Circle_1 := (X_Coordinate => 1.000, Y_Coordinate => 2.000, Radius => 5.000);
Circle_2 := (X_Coordinate => 1.005, Y_Coordinate => 2.000, Radius => 5.005);
-- These Circles are considered equal. Equality has been redefined to be
-- when the X-Y locations of the circles and their radii are both within
-- the delta.
if Circle_1 = Circle_2 then
Ada.Text_IO.Put_Line ("Circles equal.");
else
Ada.Text_IO.Put_Line ("Circles not equal.");
end if;
Circle_3 := (X_Coordinate => 1.000, Y_Coordinate => 2.000, Radius => 5.000);
Circle_4 := (X_Coordinate => 1.005, Y_Coordinate => 2.000, Radius => 5.005);
-- These Circles are not considered equal because predefined equality of
-- the extension component Radius will evaluate to False.
if Circle_3 = Circle_4 then
Ada.Text_IO.Put_Line ("Circles equal.");
else
Ada.Text_IO.Put_Line ("Circles not equal.");
end if;
end Equality_Test;
相等性應用於記錄的所有元件。當您擴充套件標記型別並比較兩個派生型別的物件以確定相等性時,將比較父元件以及新的擴充套件元件。因此,當您在標記型別上重新定義相等性並定義該型別的擴充套件時,將使用重新定義的相等性比較父元件。擴充套件元件也將被比較,使用預定義的相等性或其他一些重新定義的相等性(如果合適)。繼承的相等性的行為與其他繼承操作的行為不同。當其他基本操作被繼承時,如果您沒有覆蓋繼承的基本操作,它只能對擴充套件型別物件的父元件進行操作。另一方面,相等性通常會執行正確的事情。
- 考慮使用類範圍程式設計在構建更大、可重用、可擴充套件的框架時提供執行時動態多型性。
- 在可能的情況下,使用類範圍程式設計而不是變體記錄。
- 使用類範圍程式設計為標記型別層次結構(即類)中的型別集提供一致的介面。
- 考慮使用泛型根據現有型別定義新型別,作為擴充套件或作為容器、集合或複合資料結構。
- 避免在泛型提供更合適的機制時使用型別擴充套件來進行引數化抽象。
generic
type Element is private;
package Stack is
...
end Stack;
is preferable to:
package Stack is
type Element is tagged null record;
-- Elements to be put on the stack must be of a descendant type
-- of this type.
...
end Stack;
泛型和類範圍型別都允許單個演算法適用於多個特定型別。使用泛型,您可以在不相關的型別之間實現多型性,因為在例項化中使用的型別必須與泛型形式部分匹配。您使用泛型形式子程式指定所需的操作,並在需要時為給定例項化構建它們。泛型非常適合捕獲相對較小、可重用的演算法和程式設計習慣用法,例如排序演算法、對映、集合和迭代器。然而,隨著泛型變得越來越大,它們也變得笨拙,並且每個例項化都可能涉及額外的生成程式碼。類範圍程式設計(包括類範圍型別和型別擴充套件)更適合構建大型子系統,因為您可以避免泛型的額外生成程式碼和笨拙特性。
類範圍程式設計使您能夠獲取一組異構資料結構,併為整個集合提供一個同構的介面。另請參閱指南 9.2.1,瞭解如何使用標記型別來描述異構多型資料。
在沒有泛型功能的面向物件程式語言中,通常使用繼承來實現幾乎相同的效果。但是,這種技術通常比等效的顯式泛型定義更不清楚、更繁瑣。非泛型繼承方法始終可以使用泛型的特定例項化來恢復。另請參閱指南 5.3.2 和 5.4.7,瞭解對自引用資料結構的討論。
- 考慮賦予派生標記型別與父型別其他客戶相同的對父型別的可見性。
- 如果派生型別的實現需要比基本型別的其他客戶更高的對基本型別實現的可見性,請在定義基本型別的包的子包中定義派生標記型別。
以下示例說明了派生型別需要比基本型別的其他客戶更高的對基本型別實現的可見性。在這個棧類層次結構的示例中,Push 和 Pop 例程為所有棧變體提供了同構的介面。但是,這些操作的實現需要更高的對基本型別的可見性,因為資料元素存在差異。這個示例改編自 Barbey、Kempe 和 Strohmeier(1994)。
generic
type Item_Type is private;
package Generic_Stack is
type Abstract_Stack_Type is abstract tagged limited private;
procedure Push (Stack : in out Abstract_Stack_Type;
Item : in Item_Type) is abstract;
procedure Pop (Stack : in out Abstract_Stack_Type;
Item : out Item_Type) is abstract;
function Size (Stack : Abstract_Stack_Type) return Natural;
Full_Error : exception; -- May be raised by Push
Empty_Error : exception; -- May be raised by Pop
private
type Abstract_Stack_Type is abstract tagged limited
record
Size : Natural := 0;
end record;
end Generic_Stack;
package body Generic_Stack is
function Size (Stack : Abstract_Stack_Type)
return Natural is
begin
return Stack.Size;
end Size;
end Generic_Stack;
--
-- Now, a bounded stack can be derived in a child package as follows:
--
----------------------------------------------------------------------
generic
package Generic_Stack.Generic_Bounded_Stack is
type Stack_Type (Max : Positive) is
new Abstract_Stack_Type with private;
-- override all abstract subprograms
procedure Push (Stack : in out Stack_Type;
Item : in Item_Type);
procedure Pop (Stack : in out Stack_Type;
Item : out Item_Type);
private
type Table_Type is array (Positive range <>) of Item_Type;
type Stack_Type (Max : Positive) is new Abstract_Stack_Type with
record
Table : Table_Type (1 .. Max);
end record;
end Generic_Stack.Generic_Bounded_Stack;
----------------------------------------------------------------------
package body Generic_Stack.Generic_Bounded_Stack is
procedure Push (Stack : in out Stack_Type;
Item : in Item_Type) is
begin
-- The new bounded stack needs visibility into the base type
-- in order to update the Size element of the stack type
-- when adding or removing items.
if (Stack.Size = Stack.Max) then
raise Full_Error;
else
Stack.Size := Stack.Size + 1;
Stack.Table(Stack.Size) := Item;
end if;
end Push;
procedure Pop (Stack : in out Stack_Type;
Item : out Item_Type) is
begin
...
end Pop;
end Generic_Stack.Generic_Bounded_Stack;
如果派生型別可以在沒有對基本型別的任何特殊可見性的情況下定義,這將提供對派生型別實現與基本型別實現的變化之間的最佳解耦。另一方面,標記型別的擴充套件操作可能需要來自基本型別的一些其他資訊,這些資訊通常不需要其他客戶。
當派生標記型別的實現需要訪問基型別實現時,使用子包來定義派生型別。與其為此資訊提供額外的公共操作,不如將派生型別的定義放在子包中。這使派生型別獲得了必要的可見性,同時避免了其他客戶端的誤用風險。
當您構建具有同質介面但資料元素具有異質實現的資料結構時,很可能會出現這種情況。另請參閱指南 8.4.8、9.2.1 和 9.3.5。
Ada 提供了多種機制來支援多重繼承,其中多重繼承是一種從現有抽象逐步構建新抽象的方法,如本章開頭所定義。具體來說,Ada 支援多重繼承模組包含(透過多個 with/use 子句)、透過私有擴充套件和記錄組合實現的多重繼承“is-implemented-using”,以及透過使用泛型、形式包和訪問區分符實現的多重繼承 mixin(Taft 1994)。
- 考慮使用型別組合進行實現,而不是介面繼承。
- 考慮使用泛型將功能“混合”到某個核心抽象的派生型別中。
- 考慮使用訪問區分符來支援“完全”多重繼承,其中物件必須可作為兩個或多個不同的無關抽象的實體進行引用。
以下兩個示例直接取自 Taft(1994)。第一個示例展示瞭如何使用多重繼承技術來建立一個抽象型別,其介面從一個型別繼承,而其實現從另一個型別繼承。第二個示例展示瞭如何透過混合新功能來增強基本抽象的功能。
抽象型別 Set_Of_Strings 提供要繼承的介面
type Set_Of_Strings is abstract tagged limited private;
type Element_Index is new Natural; -- Index within set.
No_Element : constant Element_Index := 0;
Invalid_Index : exception;
procedure Enter(
-- Enter an element into the set, return the index
Set : in out Set_Of_Strings;
S : String;
Index : out Element_Index) is abstract;
procedure Remove(
-- Remove an element from the set; ignore if not there
Set : in out Set_Of_Strings;
S : String) is abstract;
procedure Combine(
-- Combine Additional_Set into Union_Set
Union_Set : in out Set_Of_Strings;
Additional_Set : Set_Of_Strings) is abstract;
procedure Intersect(
-- Remove all elements of Removal_Set from Intersection_Set
Intersection_Set : in out Set_Of_Strings;
Removal_Set : Set_Of_Strings) is abstract;
function Size(Set : Set_Of_Strings) return Element_Index
is abstract;
-- Return a count of the number of elements in the set
function Index(
-- Return the index of a given element;
-- return No_Element if not there.
Set : Set_Of_Strings;
S : String) return Element_Index is abstract;
function Element(Index : Element_Index) return String is abstract;
-- Return element at given index position
-- raise Invalid_Index if no element there.
private
type Set_Of_Strings is abstract tagged limited ...
The type Hashed_Set derives its interface from Set_of_Strings and its implementation from an existing (concrete) type Hash_Table:
type Hashed_Set(Table_Size : Positive) is
new Set_Of_Strings with private;
-- Now we give the specs of the operations being implemented
procedure Enter(
-- Enter an element into the set, return the index
Set : in out Hashed_Set;
S : String;
Index : out Element_Index);
procedure Remove(
-- Remove an element from the set; ignore if not there
Set : in out Hashed_Set;
S : String);
-- . . . etc.
private
type Hashed_Set(Table_Size : Positive) is
new Set_Of_Strings with record
Table : Hash_Table(1..Table_Size);
end record;
在包體中,您使用 Hash_Table 上可用的操作來定義操作(例如,Enter、Remove、Combine、Size 等)的體。您還必須提供任何必要的“粘合”程式碼。
在這個第二個示例中,型別 Basic_Window 對各種事件做出響應,並呼叫
type Basic_Window is tagged limited private;
procedure Display(W : Basic_Window);
procedure Mouse_Click(W : in out Basic_Window;
Where : Mouse_Coords);
. . .
您可以使用 mixin 新增諸如標籤、邊框、選單欄等功能。
generic
type Some_Window is new Window with private;
-- take in any descendant of Window
package Label_Mixin is
type Window_With_Label is new Some_Window with private;
-- Jazz it up somehow.
-- Overridden operations:
procedure Display(W : Window_With_Label);
-- New operations:
procedure Set_Label(W : in out Window_With_Label; S : String);
-- Set the label
function Label(W : Window_With_Label) return String;
-- Fetch the label
private
type Window_With_Label is
new Some_Window with record
Label : String_Quark := Null_Quark;
-- An XWindows-Like unique ID for a string
end record;
在泛型體中,您實現任何被覆蓋的操作以及新的操作。例如,您可以使用一些繼承的操作來實現被覆蓋的 Display 操作
procedure Display(W : Window_With_Label) is
begin
Display(Some_Window(W));
-- First display the window normally,
-- by passing the buck to the parent type.
if W.Label /= Null_Quark then
-- Now display the label if it is not null
Display_On_Screen(XCoord(W), YCoord(W)-5, Value(W.Label));
-- Use two inherited functions on Basic_Window
-- to get the coordinates where to display the label.
end if;
end Display;
假設您已經定義了幾個具有這些附加功能的泛型,要建立所需的視窗,您可以使用泛型例項化和私有型別擴充套件的組合,如以下程式碼所示
type My_Window is new Basic_Window with private;
. . .
private
package Add_Label is new Label_Mixin(Basic_Window);
package Add_Border is
new Border_Mixin(Add_Label.Window_With_Label);
package Add_Menu_Bar is
new Menu_Bar_Mixin(Add_Border.Window_With_Border);
type My_Window is
new Add_Menu_Bar.Window_With_Menu_Bar with null record;
-- Final window is a null extension of Window_With_Menu_Bar.
-- We could instead make a record extension and
-- add components for My_Window over and above those
-- needed by the mixins.
以下示例展示了“完全”多重繼承。
假設之前已經定義了 Savings_Account 和 Checking_Account 的包。以下示例展示了利率支票賬戶(NOW 賬戶)的定義
with Savings_Account;
with Checking_Account;
package NOW_Account is
type Object is tagged limited private;
type Savings (Self : access Object'Class) is
new Savings_Account.Object with null record;
-- These need to be overridden to call through to "Self"
procedure Deposit (Into_Account : in out Savings; ...);
procedure Withdraw (...);
procedure Earn_Interest (...);
function Interest (...) return Float;
function Balance (...) return Float;
type Checking (Self : access Object'Class) is
new Checking_Account.Object with null record;
procedure Deposit (Into_Account : in out Checking; ...);
...
function Balance (...) return Float;
-- These operations will call-through to Savings_Account or
-- Checking_Account operations. "Inherits" in this way all savings and
-- checking operations
procedure Deposit (Into_Account : in out Object; ...);
...
procedure Earn_Interest (...);
...
function Balance (...) return Float;
private
-- Could alternatively have Object be derived from either
-- Savings_Account.Object or Checking_Account.Object
type Object is tagged
record
As_Savings : Savings (Object'Access);
As_Checking : Checking (Object'Access);
end record;
end NOW_Account;
另一種可能性是,儲蓄賬戶和支票賬戶都是基於共同的 Account 抽象實現的,導致 NOW_Account.Object 兩次繼承 Balance 狀態。要解決這種歧義,您需要使用抽象型別層次結構來進行介面的多重繼承,並使用單獨的 mixin 來進行實現的多重繼承。
在 Eiffel 和 C++ 等其他語言中,多重繼承用途廣泛。例如,在 Eiffel 中,您必須使用繼承來進行模組包含和繼承本身(Taft 1994)。Ada 為模組包含提供了上下文子句,為更細緻的模組化控制提供了子庫。Ada 沒有為多重繼承提供單獨的語法。相反,它在型別擴充套件和組合中提供了一組構建塊,允許您混合額外的行為。
mixin 庫允許客戶端混合和匹配,以便開發實現。另請參閱關於實現 mixin 的指南 8.3.8。
您不應該使用多重繼承來派生一個與父類本質上無關的抽象。因此,您不應該嘗試透過從命令列型別和視窗型別繼承來派生選單抽象。但是,如果您有一個基本的抽象,例如視窗,您可以使用多重繼承 mixin 來建立一個更復雜的抽象,其中 mixin 是包含將擴充套件父抽象的型別和操作的包。
使用自引用資料結構來實現具有“完全”多重繼承(“多型性”)的型別。
一個常見的錯誤是將多重繼承用於部分關係。當一個型別由多個其他型別組成時,您應該使用指南 5.4.2 中討論的異構資料結構化技術。
- 在設計 is-a(泛化/特化)層次結構時,請考慮使用型別擴充套件。
- 使用標記型別來保留跨不同實現的通用介面(Taft 1995a)。
- 在包中定義帶標記型別時,請考慮包含對相應類寬型別的一般訪問型別的定義。
- 通常,每個包只定義一個帶標記型別。
- 以帶標記型別 T 為根的派生類中每個型別的排程操作的實現應符合對應類寬型別 T'Class 的排程操作的預期語義。
- 當型別分配必須在銷燬或覆蓋時釋放或以其他方式“清理”的資源時,請考慮使用受控型別。
- 優先使用從受控型別派生,而不是提供必須由型別客戶端呼叫的顯式“清理”操作。
- 當覆蓋從受控型別派生的調整和終結過程時,請定義終結過程以撤消調整過程的影響。
- 派生型別初始化過程應在型別特定初始化的一部分中呼叫其父級的初始化過程。
- 派生型別終結過程應在其型別特定終結的一部分中呼叫其父級的終結過程。
- 請考慮從受控型別派生資料結構的元件,而不是從受控型別派生封閉資料結構。
- 請考慮在建立分類方案(例如,分類法)時使用抽象型別和操作,其中只有葉物件在應用程式中具有意義。
- 請考慮將型別樹中的根型別和內部節點宣告為抽象。
- 請考慮將抽象型別用於泛型形式派生型別。
- 請考慮使用抽象型別來開發單個抽象的不同實現。
- 考慮基於沒有有意義的“預設”行為來宣告一個原始抽象操作。
- 考慮基於存在有意義的“預設”行為來宣告一個原始非抽象操作。
- 覆蓋操作時,覆蓋子程式不應引發被覆蓋子程式使用者不瞭解的異常。
- 如果在型別的操作實現中使用重新排程,且其特定意圖是讓某些重新排程到的操作被派生型別的專用化覆蓋,那麼在規範中作為父型別與其派生型別的“介面”的一部分清楚地記錄此意圖。
- 當在標記型別的原始操作的實現中使用重新排程(出於任何原因)時,在操作子程式的正文中記錄(以某種專案一致的方式)此使用情況,以便在維護期間能夠輕鬆找到它。
- 當可以在不知道給定標記型別的所有可能後代的情況下編寫、編譯和測試操作時,考慮使用類範圍操作(即具有類範圍型別引數的操作)(Barnes 1996)。
- 當你不希望操作被繼承和/或覆蓋時,考慮使用類範圍操作。
- 避免將建構函式宣告為原始抽象操作。
- 僅當繼承的派生型別物件不需要額外的引數進行初始化時,才使用原始抽象操作宣告初始化函式或建構函式。
- 考慮使用訪問鑑別式來提供預設初始化的引數。
- 使用建構函式進行顯式初始化。
- 考慮將物件的初始化和構造分開。
- 考慮在子包中宣告建構函式操作。
- 考慮宣告一個建構函式操作以返回對已構造物件的訪問值(Dewar 1995)。
- 當您在標記型別上重新定義 "=" 運算子時,請確保它在該型別的擴充套件中具有預期的行為,並在必要時覆蓋它。
- 考慮使用類範圍程式設計在構建更大、可重用、可擴充套件的框架時提供執行時動態多型性。
- 在可能的情況下,使用類範圍程式設計而不是變體記錄。
- 使用類範圍程式設計為標記型別層次結構(即類)中的型別集提供一致的介面。
- 考慮使用泛型根據現有型別定義新型別,作為擴充套件或作為容器、集合或複合資料結構。
- 避免在泛型提供更合適的機制時使用型別擴充套件來進行引數化抽象。
- 考慮賦予派生標記型別與父型別其他客戶相同的對父型別的可見性。
- 如果派生型別的實現需要比基本型別的其他客戶更高的對基本型別實現的可見性,請在定義基本型別的包的子包中定義派生標記型別。
- 考慮使用型別組合進行實現,而不是介面繼承。
- 考慮使用泛型將功能“混合”到某個核心抽象的派生型別中。
- 考慮使用訪問區分符來支援“完全”多重繼承,其中物件必須可作為兩個或多個不同的無關抽象的實體進行引用。