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)。
- 当您在标记类型上重新定义 "=" 运算符时,请确保它在该类型的扩展中具有预期的行为,并在必要时覆盖它。
- 考虑使用类范围编程在构建更大、可重用、可扩展的框架时提供运行时动态多态性。
- 在可能的情况下,使用类范围编程而不是变体记录。
- 使用类范围编程为标记类型层次结构(即类)中的类型集提供一致的接口。
- 考虑使用泛型根据现有类型定义新类型,作为扩展或作为容器、集合或复合数据结构。
- 避免在泛型提供更合适的机制时使用类型扩展来进行参数化抽象。
- 考虑赋予派生标记类型与父类型其他客户相同的对父类型的可见性。
- 如果派生类型的实现需要比基本类型的其他客户更高的对基本类型实现的可见性,请在定义基本类型的包的子包中定义派生标记类型。
- 考虑使用类型组合进行实现,而不是接口继承。
- 考虑使用泛型将功能“混合”到某个核心抽象的派生类型中。
- 考虑使用访问区分符来支持“完全”多重继承,其中对象必须可作为两个或多个不同的无关抽象的实体进行引用。