Ada 样式指南/程序结构
适当的结构可以提高程序清晰度。这类似于较低级别的可读性,并有助于使用可读性指南(第 3 章)。Ada 提供的各种程序结构化功能旨在提高整体设计清晰度。这些指南展示了如何将这些功能用于其预期目的。
子包的概念支持子系统概念,其中子系统在 Ada 中表示为库单元的层次结构。一般而言,大型系统应结构化为一系列子系统。子系统应用于表示逻辑相关的库单元,这些单元共同实现单个、高级抽象或框架。
抽象和封装由包概念和私有类型支持。相关数据和子程序可以组合在一起,并被更高层级视为单个实体。信息隐藏通过强类型化和将包和子程序规范与其主体分离来强制执行。例外和任务是影响程序结构的额外 Ada 语言元素。
结构良好的程序易于理解、增强和维护。结构不良的程序在维护过程中经常被重新结构化,只是为了让工作更容易。下面列出的许多指南通常作为一般程序设计指南给出。
- 将每个库单元包的规范放在与其主体不同的文件中。
- 避免定义不打算用作主程序的库单元子程序。如果定义了此类子程序,则为每个库单元子程序创建一个显式规范,在单独的文件中。
- 最小化子单元的使用。
- 优先使用子库单元,而不是子单元,将子系统结构化为易于管理的单元。
- 将每个子单元放在单独的文件中。
- 使用一致的文件命名约定。
- 优先使用私有子单元规范,而不是嵌套在包主体中,并使用它来扩展父单元的抽象或服务。
- 将私有子单元规范用于(其他)子单元需要的数据和子程序,这些子单元扩展了父单元的抽象或服务。
以下文件名说明了一种可能的文件组织和相关的一致命名约定。库单元名称对主体使用 adb 后缀。后缀 ads 表示规范,任何包含子单元的文件都使用通过用下划线分隔主体名称和子单元名称来构造的名称
text_io.ads — the specification text_io.adb — the body text_io_integer_io.adb — a subunit text_io_fixed_io.adb — a subunit text_io_float_io.adb — a subunit text_io_enumeration_io.adb — a subunit
根据您的文件系统允许您在文件名中使用的字符,您可以在文件名中更清楚地显示父级名称和子单元名称之间的区别。例如,如果您的文件系统允许使用“#”字符,则可以使用“#”分隔主体名称和子单元名称
text_io.ads — the specification text_io.adb — the body text_io#integer_io.adb — a subunit text_io#fixed_io.adb — a subunit text_io#float_io.adb — a subunit text_io#enumeration_io.adb — a subunit
某些操作系统区分大小写,尽管 Ada 本身不区分大小写。例如,您可以选择使用全小写文件名的约定。
本指南强调将规格说明和主体文件分开的主要原因是为了最大程度地减少每次修改后所需的重新编译次数。通常,在软件开发过程中,单元主体比规格说明更新频率更高。如果主体和规格说明位于同一个文件中,那么每次编译主体时,即使规格说明没有改变,也会对其进行编译。由于规格说明定义了单元与其所有使用者之间的接口,因此对规格说明的这种重新编译通常需要重新编译所有使用者以验证它们是否符合规格说明。如果使用者的规格说明和主体也一起存放,那么这些单元的任何使用者也将需要重新编译,以此类推。这种连锁反应可能会迫使大量原本可以避免的编译,严重拖慢项目的开发和测试阶段。这就是为什么你应该将所有库单元(非嵌套单元)的规格说明放在与主体文件不同的文件中。
库单元子程序应尽量简化。库单元子程序的唯一实际用途是作为主子程序。在几乎所有其他情况下,将子程序嵌入到包中会更好。这提供了一个位置(包主体)来局部化子程序所需的数据。此外,它还能减少系统中单独模块的数量。
一般来说,你应该为在 with 子句中提到的任何库单元子程序使用单独的规格说明。这使得 with 的单元依赖于库单元子程序的规格说明,而不是其主体。
你应该尽量减少使用子单元,因为它们会产生维护问题。出现在父主体中的声明在子单元中可见,这增加了对子单元全局的数据量,从而增加了更改的潜在连锁反应。子单元阻碍了重用,因为它们鼓励将原本可重用的代码直接放入子单元,而不是放到从多个子程序调用的公共例程中。
随着 Ada 95 中子库单元的出现,你可以避免大多数对子单元的使用。例如,与其使用子单元来实现一个大型嵌套主体,你应该尝试将此代码封装到一个子库单元中,并添加必要的上下文子句。你可以修改子单元的主体,而无需重新编译子系统中的任何其他单元。
使用多个独立文件的一个额外好处是,它允许不同的实现者使用传统的编辑器同时修改系统的不同部分,这些编辑器不允许对单个文件进行多个并发更新。
最后,将主体和规格说明分开使得可以为同一个规格说明提供多个主体,或者为同一个主体提供多个规格说明。尽管 Ada 要求在任何给定时间系统中每个主体都只有一个规格说明,但维护多个主体或多个规格说明以用于系统的不同构建仍然很有用。例如,一个规格说明可以有多个主体,每个主体都以不同的时间与空间效率权衡来实现相同的功能,或者对于机器依赖代码,每个目标机器可能有一个主体。在开发和测试期间,维护多个包规格说明也很有用。你可以开发一个规格说明交付给客户,另一个规格说明用于单元测试。第一个只导出在系统正常运行期间从包外部调用的那些子程序。第二个将导出包的所有子程序,以便可以独立测试每个子程序。
建议使用一致的文件命名约定,以便更容易管理根据本指南可能产生的大量文件。
在实现包规格说明中定义的抽象时,你通常需要编写支持子程序来操纵数据的内部表示。这些子程序不应在接口上导出。你可以选择将它们放在父程序的包主体中,或者放在父包主体上下文子句中命名的子包中。当你在父包主体中放置它们时,你使它们对父包的所有客户都不可访问,包括在子包中声明的父包的扩展。如果这些子程序需要实现父抽象的扩展,你将不得不修改父规格说明和主体,因为你必须在父规格说明中声明扩展。这种技术将迫使重新编译整个包(规格说明和主体)以及所有其客户。
或者,你可以在私有子包中实现支持子程序。由于父单元的规格说明没有修改,因此它及其客户不需要重新编译。在父单元主体中可能声明的数据和子程序现在必须在私有子包的规格说明中声明,以使它们对父单元主体以及扩展父单元服务或抽象的任何子单元可见。(另请参见指南 4.1.6 和 4.2。)这种私有子包的使用通常会最大程度地减少单元族及其客户之间的重新编译次数。
在声明子包为私有时,你获得了与在父包主体中声明它类似的效果,因为父包的客户不能在上下文子句中命名私有子包。你获得了灵活性,因为现在你可以使用子包扩展父抽象,而无需重新编译父规格说明或其主体,假设你没有修改父包或其主体。这种额外的灵活性通常会弥补单元之间依赖性的增加,在本例中,父主体(和其他子包主体)上的附加上下文子句命名了支持子程序的私有子包。
配置编译指示
[edit | edit source]指南
[edit | edit source]- 如果可能,通过编译器选项或其他不需要修改源代码的方法来表达配置编译指示。
- 当配置编译指示必须放在源代码中时,考虑将它们隔离到每个分区的一个编译单元;如果指定,建议使用该分区的 main 子程序。
原理
[edit | edit source]配置编译指示通常用于选择分区范围或系统范围的选项。通常,它们反映高级软件架构决策(例如,pragma Task_Dispatching_Policy)或在特定应用领域中使用软件(例如,安全关键软件)。如果配置编译指示嵌入到软件组件中,并且该组件在不再适用该编译指示的不同上下文中被重用,那么它可能会在新的应用程序中引起问题。此类问题可能包括编译系统拒绝其他合法源代码或运行时出现意外行为。鉴于配置编译指示的广泛范围,这些问题可能很严重。此外,原始系统的维护可能需要更改一些这些系统范围的决策。如果配置编译指示散布在整个软件中,那么可能难以找到需要更改的行。
因此,建议所有配置编译指示都尽可能保存在单个编译单元中,以便于定位和修改。如果此编译单元不太可能被重用(例如,主子程序),那么与未来重用者发生冲突的可能性就会降低。最后,如果这些系统范围的决策完全没有嵌入到代码中,而是通过编译器选项等方式指示,那么上述问题发生的可能性就更小。
例外情况
[edit | edit source]某些编译指示(例如,pragma Suppress)可以使用多种形式,包括作为配置编译指示。本指南不适用于这些编译指示在不作为配置编译指示的情况下使用。
子程序
[edit | edit source]指南
[edit | edit source]- 使用子程序来增强抽象。
- 将每个子程序限制为执行单个操作。
示例
[edit | edit source]你的程序需要作为菜单驱动用户界面包的一部分绘制用户选项菜单。由于菜单的内容会根据用户状态而改变,因此正确的方法是编写一个子程序来绘制菜单。这样,输出子程序就只有一个目的,并且确定菜单内容的方法在其他地方描述。
...
----------------------------------------------------------------------
procedure Draw_Menu
(Title : in String;
Options : in Menu) is
...
begin -- Draw_Menu
Ada.Text_IO.New_Page;
Ada.Text_IO.New_Line;
Ada.Text_IO.Set_Col (Right_Column);
Ada.Text_IO.Put_Line (Title);
Ada.Text_IO.New_Line;
for Choice in Alpha_Numeric loop
if Options (Choice) /= Empty_Line then
Valid_Option (Choice) := True;
Ada.Text_IO.Set_Col (Left_Column);
Ada.Text_IO.Put (Choice & " -- ");
Ada.Text_IO.Put_Line (Options (Choice));
end if;
...
end loop;
end Draw_Menu;
----------------------------------------------------------------------
原理
[edit | edit source]子程序是一种非常有效且为人熟知的抽象技术。子程序通过隐藏特定活动的细节来提高程序的可读性。子程序不必被调用多次才能证明其存在。
注释
[edit | edit source]指南 10.7.1 讨论了处理子程序调用开销的问题。
函数
[edit | edit source]指南
[edit | edit source]- 当子程序的主要目的是提供单个值时,使用函数。
- 尽量减少函数的副作用。
- 如果值不需要是静态的,请考虑使用无参数函数。
- 如果值应该被从类型派生的类型继承,请使用无参数函数(而不是常量)。
- 如果值本身可能会改变,请使用无参数函数。
示例
[edit | edit source]虽然从文件中读取一个字符会改变接下来读取的字符,但这被认为是次要的副作用,与以下函数的主要目的相比。
function Next_Character return Character is separate;
但是,使用这样的函数可能会导致一个微妙的问题。任何时候评估顺序都是未定义的,函数返回的值的顺序将实际上是未定义的。在这个例子中,放置在 Word 中的字符的顺序以及后面的两个字符传递给 Suffix 参数的顺序是未知的。Next_Character 函数的任何实现都无法保证哪个字符将放在哪里。
Word : constant String := String'(1 .. 5 => Next_Character);
begin -- Start_Parsing
Parse(Keyword => Word,
Suffix1 => Next_Character,
Suffix2 => Next_Character);
end Start_Parsing;
当然,如果顺序不重要(如在随机数生成器中),那么评估顺序就不重要。
以下示例显示了使用无参数函数而不是常量的用法。
type T is private;
function Nil return T; -- This function is a derivable operation of type T
function Default return T; -- Also derivable, and the value can be changed by
-- recompiling the body of the function
同样的示例可以使用常量编写。
type T is private;
Nil : constant T;
Default : constant T;
原理
[edit | edit source]副作用是对任何不是子程序局部变量的变量的改变。这包括在函数返回后,其他子程序和条目在从函数调用期间对变量的更改,如果更改仍然存在。副作用是不鼓励的,因为它们难以理解和维护。此外,Ada 语言没有定义在表达式中或作为子程序的实际参数时,函数的评估顺序。因此,依赖于函数副作用发生顺序的程序是错误的。任何地方都要避免使用副作用。
包
[edit | edit source]指南
[edit | edit source]- 使用包进行信息隐藏。
- 使用带有标记类型和私有类型的包来创建抽象数据类型。
- 使用包来模拟与问题领域相关的抽象实体。
- 使用包将相关的类型和对象声明分组在一起(例如,用于两个或多个库单元的通用声明)。
- 将机器依赖项封装在包中。将特定设备的软件接口放在包中,以方便更改到不同的设备。
- 将低级实现决策或接口放在包中的子程序中。
- 使用包和子程序来封装和隐藏可能更改的程序细节(Nissen 和 Wallis 1984)。
示例
[edit | edit source]读取外部文件的名称和其他属性高度依赖于机器。名为 Directory 的包可以包含类型和子程序声明,以支持对包含外部文件的外部目录的广义视图。它的内部可能反过来依赖于更具体的硬件或操作系统的其他包。
package Directory is
type Directory_Listing is limited private;
procedure Read_Current_Directory (D : in out Directory_Listing);
generic
with procedure Process (Filename : in String);
procedure Iterate (Over : in Directory_Listing);
...
private
type Directory_Listing is ...
end Directory;
---------------------------------------------------------------
package body Directory is
-- This procedure is machine dependent
procedure Read_Current_Directory (D : in out Directory_Listing) is separate;
procedure Iterate (Over : in Directory_Listing) is
...
begin
...
Process (Filename);
...
end Iterate;
...
end Directory;
原理
[edit | edit source]包是 Ada 中主要的结构化机制。它们旨在作为抽象、信息隐藏和模块化的直接支持。例如,它们对封装机器依赖性以帮助移植很有用。单个规范可以具有多个主体,隔离实现特定的信息,以便代码的其他部分不需要更改。
封装潜在更改区域有助于通过防止系统无关部分之间的不必要依赖关系来最大限度地减少实现该更改所需的工作量。
注释
[edit | edit source]对本指南最普遍的异议通常涉及性能损失。有关子程序开销的讨论,请参见指南 10.7.1。
子库单元
[edit | edit source]指南
[edit | edit source]- 如果一个新的库单元代表了对原始抽象的逻辑扩展,请将其定义为子库单元。
- 如果一个新的库单元是独立的(例如,引入了仅部分依赖于现有抽象的新抽象),那么将新抽象封装在单独的库单元中。
- 使用子包来实现子系统。
- 对子系统中应该对子系统客户机可见的部分使用公共子单元。
- 对子系统中不应该对子系统客户机可见的部分使用私有子单元。
- 对仅在实现包规范中使用的局部声明使用私有子单元。
- 使用子包来实现构造函数,即使它们返回访问值。
示例
[edit | edit source]以下窗口系统示例取自 Cohen 等人 (1993),它说明了子单元在设计子系统中的一些用法。父级(根)包声明其客户机和子系统需要的类型、子类型和常量。各个子包提供窗口抽象的特定部分,例如原子、字体、图形输出、光标和键盘信息。
package X_Windows is
...
private
...
end X_Windows;
package X_Windows.Atoms is
type Atom is private;
...
private
...
end X_Windows.Atoms;
package X_Windows.Fonts is
type Font is private;
...
private
...
end X_Windows.Fonts;
package X_Windows.Graphic_Output is
type Graphic_Context is private;
type Image is private;
...
private
...
end X_Windows.Graphic_Output;
package X_Windows.Cursors is
...
end X_Windows.Cursors;
package X_Windows.Keyboard is
...
end X_Windows.Keyboard;
原理
[edit | edit source]用户可以使用更少的混乱的接口创建更精确的包,使用子库包根据需要扩展接口。父级只包含相关功能。父级提供一个通用的接口,而子单元提供更完整的编程接口,根据它们正在扩展或定义的抽象的该方面进行定制。
子包建立在 Ada 的模块化优势之上,其中“不同的规范和主体将包的用户界面(规范)与其实现(主体)分离”(Rationale 1995,§II.7)。子包提供额外的功能,能够扩展父包,而无需重新编译父包或父包的客户机。
子包允许您编写逻辑上不同的包,它们共享私有类型。可见性规则使子规范的私有部分和子体的体对父级的私有部分具有可见性。因此,您可以避免为开发共享私有类型并需要了解其表示的抽象而创建单片包。私有表示对包的客户不可见,因此包及其子类中的抽象得以维护。
对局部声明使用私有子包使您能够在实现父包及其父包的扩展时获得所需的支撑声明。通过使用一组通用的支撑声明(数据表示、数据操作子程序),您可以提高程序的可维护性。您可以修改内部表示和支撑子程序的实现,而无需修改或重新编译子系统的其余部分,因为这些支撑子程序是在私有子包的体中实现的。另请参见指南 4.1.1、4.2.1、8.4.1 和 8.4.8。
另请参见指南 9.4.1,了解在创建标记类型层次结构中使用子库单元的讨论。
内聚性
[edit | edit source]指南
[edit | edit source]- 使每个包都服务于单一目的。
- 使用包来分组相关数据、类型和子程序。
- 避免不相关的对象和子程序的集合(NASA 1987;Nissen 和 Wallis 1984)。
- 考虑对系统进行重构,将两个高度相关的单元移入同一个包(或包层次结构)中,或者将相对独立的单元移入单独的包中。
示例
[edit | edit source]作为一个不好的例子,名为 Project_Definitions 的包显然是特定项目的“万能包”,很可能是一个混乱的集合。它可能采用这种形式,以允许项目成员将单个 with 子句合并到他们的软件中。
更好的例子是名为 Display_Format_Definitions 的包,其中包含特定格式的特定显示器所需的所有类型和常量,以及 Cartridge_Tape_Handler,其中包含所有提供与专用设备接口的类型、常量和子程序。
原理
[edit | edit source]包中实体的相关程度直接影响对包和由包组成的程序的理解程度。分组有不同的标准,有些标准的效果不如其他标准。根据数据或活动的类别(例如,初始化模块)进行分组,或者根据数据或活动的时间特征进行分组,效果不如根据功能或通过数据进行通信的需要进行分组(Charette 1986)。
系统的“正确”结构可以对系统的可维护性产生巨大的影响。虽然在当时可能看起来很痛苦,但如果最初的结构不太正确,重要的是进行重构。
另请参见关于异构数据的指南 5.4.2。
注释
[edit | edit source]传统的子程序库通常将功能无关的子程序分组在一起。即使这样的库也应该被分解成一组包,每个包都包含一组逻辑上内聚的子程序。
数据耦合
[edit | edit source]指南
[edit | edit source]- 避免在包规范中声明变量。
示例
[edit | edit source]这是编译器的一部分。处理错误消息的包和包含代码生成器的包都需要知道当前的行号。与其将此存储在 Natural 类型的共享变量中,不如将其存储在一个隐藏此类信息表示细节并通过访问例程使其可用的包中。
-------------------------------------------------------------------------
package Compilation_Status is
type Line_Number is range 1 .. 2_500_000;
function Source_Line_Number return Line_Number;
end Compilation_Status;
-------------------------------------------------------------------------
with Compilation_Status;
package Error_Message_Processing is
-- Handle compile-time diagnostic.
end Error_Message_Processing;
-------------------------------------------------------------------------
with Compilation_Status;
package Code_Generation is
-- Operations for code generation.
end Code_Generation;
-------------------------------------------------------------------------
原理
[edit | edit source]紧密耦合的程序单元可能难以调试,并且非常难以维护。通过使用访问函数保护共享数据,耦合程度降低。这可以防止对数据结构的依赖,并且可以控制对数据的访问。
注释
[edit | edit source]对该指南最普遍的反对意见通常涉及性能损失。当变量移动到包主体时,必须提供访问变量的子程序,并且在每次调用这些子程序时都会引入相关的开销。有关子程序开销的讨论,请参见指南 10.7.1。
任务
[edit | edit source]指南
[edit | edit source]- 使用任务来模拟问题域中的抽象、异步实体。
- 使用任务为多处理器体系结构定义并发算法。
- 使用任务执行并发、循环或优先级活动(NASA 1987)。
原理
[edit | edit source]该指南的理由在指南 6.1.2 中给出。第 6 章更详细地讨论了任务。
保护类型
[edit | edit source]指南
[edit | edit source]- 使用保护类型来控制或同步对数据或设备的访问。
- 使用保护类型来实现同步任务,例如被动资源监视器。
示例
[edit | edit source]请参见指南 6.1.1 中的示例。
原理
[edit | edit source]该指南的理由在指南 6.1.1 中给出。第 6 章更详细地讨论了并发和保护类型。
可见性
[edit | edit source]Ada 通过其可见性控制功能来强制信息隐藏和关注点分离的能力是该语言最重要的优势之一,特别是在“大型系统的各个部分被单独开发时”。破坏这些功能,例如过度依赖 use 子句,是浪费和危险的。另请参见指南 5.7 和 9.4.1。
接口最小化
[edit | edit source]指南
[edit | edit source]- 仅将包使用所需的內容放入其规范中。
- 将包规范中的声明数量降至最低。
- 不要仅仅因为构建简单而包含额外的操作。
- 将包规范中的上下文(with)子句降至最低。
- 重新考虑似乎需要大量参数的子程序。
- 不要仅仅为了限制参数数量而在子程序或包中操作全局数据。
- 避免不必要的可见性;将程序单元的实现细节隐藏在其用户之外。
- 使用子库单元来控制子系统接口部分的可见性。
- 对于那些不应该在子系统外部使用的声明,使用私有子包。
- 使用子库单元为不同的客户端呈现实体的不同视图。
- 在确定了接口预期客户端的逻辑之后,设计(和重新设计)接口。
示例
[edit | edit source]-------------------------------------------------------------------------
package Telephone_Book is
type Listing is limited private;
procedure Set_Name (New_Name : in String;
Current : in out Listing);
procedure Insert (Name : in String;
Current : in out Listing);
procedure Delete (Obsolete : in String;
Current : in out Listing);
private
type Information;
type Listing is access Information;
end Telephone_Book;
-------------------------------------------------------------------------
package body Telephone_Book is
-- Full details of record for a listing
type Information is
record
...
Next : Listing;
end record;
First : Listing;
procedure Set_Name (New_Name : in String;
Current : in out Listing) is separate;
procedure Insert (Name : in String;
Current : in out Listing) is separate;
procedure Delete (Obsolete : in String;
Current : in out Listing) is separate;
end Telephone_Book;
-------------------------------------------------------------------------
原理
[edit | edit source]对于规范中的每个实体,都要仔细考虑是否可以将其移到子包或父包主体中。无关细节越少,程序、包或子程序就越容易理解。对于维护人员来说,了解包接口究竟是什么非常重要,这样他们才能理解更改的影响。子程序的接口超出了参数范围。从包或子程序内部对全局数据的任何修改也是对“外部”的未记录接口。
通过将不必要的子句移到主体中来将规范上的上下文子句降至最低。这种技术使读者更容易,将库单元更改时所需的重新编译局部化,并有助于防止修改过程中的连锁反应。另请参见指南 4.2.3。
具有大量参数的子程序通常表明设计决策不当(例如,子程序的功能边界不合适,或者参数结构不佳)。相反,没有参数的子程序可能会访问全局数据。
在包规范中可见的对象可以被任何有权访问它们的单元修改。该对象不能被其封闭包保护或抽象地表示。必须持久化的对象应在包体中声明。其值依赖于其封闭包外部程序单元的对象可能位于错误的包中,或者最好通过包规范中指定的子程序访问。
子库单元可以提供分层库的不同视图。工程师可以为客户提供与实现者不同的视图(基本原理 1995,第 10.1 节)。通过创建私有子包,工程师可以提供仅在父库单元根目录下的子系统内部可用的设施。私有子包规范中的声明不会导出到子系统外部。因此,工程师可以在私有子包中声明实现抽象所需的实用程序(例如,调试实用程序 [Cohen 等人 1993]),并确保抽象的用户(即客户端)无法访问这些实用程序。
不同的客户端可能对本质上相同的资源有不同的需求。与其创建多个版本的资源,不如考虑使用子单元,为不同的目的导出不同的视图。
严格基于预测客户端“可能”需要的需求来设计接口会导致接口臃肿和不合适。然后发生的情况是,客户端试图“适应”接口并绕过不合适的接口,重复逻辑上应该是共享抽象的一部分的代码。有关从可重用性角度考虑接口的讨论,请参见指南 8.3.1。
在某些情况下,子程序库看起来像大型的单片包。在这种情况下,将它们分解成更小的包,根据类别对它们进行分组(例如,三角函数),可能会很有益。
- 使用子包而不是嵌套包来呈现同一抽象的不同视图。
- 仅为了分组操作或隐藏公共实现细节,才将包规范嵌套在另一个包规范中。
Ada 参考手册(1995)的附件 A 给出了包规范嵌套的示例。泛型包 Generic_Bounded_Length 的规范嵌套在包 Ada.Strings.Bounded 的规范中。嵌套的包是泛型的,它将紧密相关的操作分组在一起。
将包规范分组到一个包含的包中,强调了这些包之间共性的关系。它还允许它们共享从这种关系产生的公共实现细节。嵌套包允许你组织包的命名空间,这与在子程序或任务体中嵌套的语义效果形成对比。
抽象有时需要向不同类别的用户呈现不同的视图。将一个视图构建到另一个视图之上作为额外的抽象并不总是足够的,因为视图呈现的操作的功能可能只是部分分离。嵌套规范将各种视图的设施分组在一起,但仍将其与它们呈现的抽象关联在一起。由于存在多个使用子句或不协调的限定名称组合,另一个单元对视图的滥用混合将很容易检测到。
请参见指南 4.2.1 中讨论的基本原理。
- 考虑使用私有子包代替嵌套。
- 如果不能使用私有子包,则应尽可能地限制程序单元的可见性,方法是在包体中嵌套它们(Nissen 和 Wallis 1984)。
- 最大程度地减少在子程序和任务中嵌套程序单元。
- 最大程度地减少 with 子句适用的范围。
- 仅使用那些直接需要的单元。
此程序说明了使用子库单元来限制可见性。过程 Rational_Numbers.Reduce 嵌套在 Rational_Numbers 的主体中,以将其可见性限制在该抽象的实现中。与其将文本输入/输出设施对整个有理数层次结构可见,不如只将其对子库 Rational_Numbers.IO 的主体可见。此示例改编自 Ada 参考手册(1995,第 7.1 [带注释]、7.2 [带注释] 和 10.1.1 [带注释] 节)。
-------------------------------------------------------------------------
package Rational_Numbers is
type Rational is private;
function "=" (X, Y: Rational) return Boolean;
function "/" (X, Y: Integer) return Rational; -- construct a rational number
function "+" (X, Y: Rational) return Rational;
function "-" (X, Y: Rational) return Rational;
function "*" (X, Y: Rational) return Rational;
function "/" (X, Y: Rational) return Rational; -- rational division
private
...
end Rational_Numbers;
package body Rational_Numbers is
procedure Reduce (R :in out Rational) is . . . end Reduce;
. . .
end Rational_Numbers;
package Rational_Numbers.IO is
procedure Put (R : in Rational);
procedure Get (R : out Rational);
end Rational_Numbers.IO;
with Ada.Text_IO;
with Ada.Integer_Text_IO;
package body Rational_Numbers.IO is -- has visibility to parent private type declaration
procedure Put (R : in Rational) is
begin
Ada.Integer_Text_IO.Put (Item => R.Numerator, Width => 0);
Ada.Text_IO.Put ("/");
Ada.Integer_Text_IO.Put (Item => R.Denominator, Width => 0);
end Put;
procedure Get (R : out Rational) is . . . end Get;
end Rational_Numbers.IO;
限制程序单元的可见性可确保程序单元不会从系统中除预期位置之外的其他地方调用。这可以通过将其嵌套在唯一使用它的单元中,通过将其隐藏在包体中而不是在包规范中声明它,或者通过将其声明为私有子单元来完成。这避免了错误,并通过保证对该单元的本地更改不会产生不可预见的影响,从而简化了维护人员的工作。
通过对子单元使用 with 子句而不是对整个父单元使用 with 子句来限制库单元的可见性,在相同方面很有用。在上面的示例中,很明显包 Text_IO 仅由编译器的 Listing_Facilities 包使用。
不鼓励在子程序和任务中进行嵌套,因为这会导致不可重用的组件。这些组件本质上不可重用,因为它们对定义上下文进行了不希望的上级引用。除非你确实想要确保程序单元不会从系统中某个意外的位置调用,否则应最大程度地减少这种嵌套形式。
有关子单元使用的讨论,另请参见指南 4.2.1。
最小化 with 子句覆盖范围的一种方法是仅将其用于真正需要它的子单元。当对库单元的可见性需求仅限于一两个子程序时,请考虑将这些子单元作为单独的编译单元。
- 仔细考虑任务的封装。
-------------------------------------------------------------------------
package Disk_Head_Scheduler is
type Words is ...
type Track_Number is ...
procedure Transmit (Track : in Track_Number;
Data : in Words);
...
end Disk_Head_Scheduler;
-------------------------------------------------------------------------
package body Disk_Head_Scheduler is
...
task Control is
entry Sign_In (Track : in Track_Number);
...
end Control;
----------------------------------------------------------------------
task Track_Manager is
entry Transfer(Track_Number) (Data : in Words);
end Track_Manager;
----------------------------------------------------------------------
...
procedure Transmit (Track : in Track_Number;
Data : in Words) is
begin
Control.Sign_In(Track);
Track_Manager.Transfer(Track)(Data);
end Transmit;
----------------------------------------------------------------------
...
end Disk_Head_Scheduler;
-------------------------------------------------------------------------
是否在封闭包的规范或主体中声明任务,这并非易事。两者都有充分的论据。
将任务规范隐藏在包体中,并仅通过(子程序)导出必要的条目,可以减少包规范中多余的信息。它允许您的子程序强制执行任务正常运行所需的任何条目调用顺序。它还允许您实施防御性任务通信实践(参见指南 6.2.2)以及正确使用条件和定时条目调用。最后,它允许将条目分组为集合,以便导出到不同类别的用户(例如,生产者与消费者),或者隐藏不应该公开的条目(例如,初始化、完成、信号)。在性能是一个问题,并且没有顺序规则需要强制执行的情况下,可以将条目重命名为子程序,以避免额外过程调用的开销。
一个论点,可以看作是优点或缺点,是隐藏包体中的任务规范隐藏了任务实现的事实。如果应用程序是这样的,即对任务实现的更改或从任务实现的更改或任务之间服务的重组不需要让包的用户关心,那么这是一个优点。但是,如果包用户必须了解任务实现才能推断全局任务行为,那么最好不要完全隐藏任务。要么将其移动到包规范中,要么添加注释说明存在任务实现,描述何时调用可能会阻塞等等。否则,包实现者有责任确保包的用户不必担心死锁、饥饿和竞争条件等行为。
最后,请记住,隐藏在过程接口后面的任务会阻止使用条件和定时条目调用以及条目族,除非您在过程中添加参数和额外代码,以使调用者能够将过程定向到使用这些功能。
本节探讨了程序结构中异常情况的问题。它讨论了如何在单元接口中使用异常情况,包括声明和引发哪些异常,以及在什么条件下引发异常。有关如何处理、传播和避免引发异常的信息,请参阅指南 5.8。有关如何处理可移植性问题的指南,请参阅指南 7.5。
- 对于无法避免的内部错误,用户无法恢复,请声明一个用户可见的异常。在抽象内部,提供一种方法来区分不同的内部错误。
- 不要从其他上下文中借用异常名称。
- 导出(对用户可见地声明)所有可能引发的异常的名称。
- 在包中,记录每个子程序和任务条目可能引发哪些异常。
- 不要为可以在单元内避免或纠正的内部错误引发异常。
- 不要使用同一个异常来报告单元用户可以区分的不同类型的错误。
- 提供询问函数,允许单元用户避免引发异常。
- 如果可能,请在引发异常之前避免更改单元中的状态信息。
- 在最早的机会捕获和转换或处理所有预定义的和编译器定义的异常。
- 不要显式引发预定义的或实现定义的异常。
- 永远不要让异常传播到其范围之外。
此包规范定义了两个异常,它们增强了抽象
-------------------------------------------------------------------------
generic
type Element is private;
package Stack is
function Stack_Empty return Boolean;
function Stack_Full return Boolean;
procedure Pop (From_Top : out Element);
procedure Push (Onto_Top : in Element);
-- Raised when Pop is used on empty stack.
Underflow : exception;
-- Raised when Push is used on full stack.
Overflow : exception;
end Stack;
-------------------------------------------------------------------------
...
----------------------------------------------------------------------
procedure Pop (From_Top : out Element) is
begin
...
if Stack_Empty then
raise Underflow;
else -- Stack contains at least one element
Top_Index := Top_Index - 1;
From_Top := Data(Top_Index + 1);
end if;
end Pop;
--------------------------------------------------------------------
...
异常应该用作抽象的一部分,以指示抽象无法阻止或纠正的错误条件。因为抽象无法纠正此类错误,所以它必须将错误报告给用户。在使用错误(例如,尝试以错误的顺序调用操作或尝试超过边界条件)的情况下,用户可能能够纠正错误。在超出用户控制范围的错误情况下,如果有多种机制可用于执行所需操作,用户可能能够解决错误。在其他情况下,用户可能不得不放弃使用该单元,进入功能有限的降级模式。无论哪种情况,都必须通知用户。
异常是报告此类错误的良好机制,因为它们为处理错误提供了备用控制流程。这允许错误处理代码与正常处理代码分开。当引发异常时,当前操作将中止,控制权将直接转移到适当的异常处理程序。
上面的几个指南是为了最大限度地提高用户区分和纠正不同类型错误的能力。声明新的异常名称,而不是引发在其他包中声明的异常,可以减少包之间的耦合,还可以使不同的异常更易于区分。导出单元可以引发的所有异常的名称,而不是在单元内部声明它们,使得单元用户可以在异常处理程序中引用这些名称。否则,用户只能使用 others 处理程序来处理异常。最后,使用注释来准确记录包中声明的哪些异常可能被每个子程序或任务条目引发,从而使用户能够知道在每种情况下哪些异常处理程序是合适的。
在存在抽象用户无法采取任何明智操作的错误情况(例如,没有解决方法或降级模式)的情况下,最好导出单个内部错误异常。在包内,您应该考虑区分不同的内部错误。例如,您可以以不同的方式记录或处理不同类型的内部错误。但是,当您将错误传播给用户时,您应该使用特殊的内部错误异常,表明无法进行用户恢复。在传播错误时,您还应该提供相关信息,使用 Ada.Exceptions 中提供的设施。因此,对于任何抽象,您实际上提供了 N + 1 个不同的异常:N 个不同的可恢复错误和一个不可恢复错误,它们没有映射到抽象。应用程序需求以及客户在错误信息方面的需求/愿望都会帮助您为抽象识别合适的异常。
因为它们会导致立即转移控制权,所以异常对于报告不可恢复的错误很有用,这些错误会阻止操作完成,但不会报告操作完成的附带状态或模式。它们不应用于报告单元能够在用户不可见的情况下纠正的内部错误。
为了为用户提供最大的灵活性,最好提供询问函数,用户可以调用这些函数来确定如果调用子程序或任务条目是否会引发异常。上面的示例中的 Stack_Empty 函数就是这样一种函数。它指示如果调用 Pop 是否会引发 Underflow。提供此类函数使用户能够避免触发异常。
为了支持其用户的错误恢复,单元应该尝试避免在引发异常的调用期间更改状态。如果无法完全正确地执行请求的操作,那么单元应该在更改任何内部状态信息之前检测到这一点,或者应该恢复到请求时的状态。例如,在引发 Underflow 异常后,上面的堆栈包应该保持与调用 Pop 时完全相同的状态。如果它要针对管理堆栈的内部数据结构进行部分更新,那么以后的 Push 和 Pop 操作将无法正确执行。这始终是可取的,但并非总是可行的。
应该使用用户定义的异常而不是预定义的或编译器定义的异常,因为它们更具描述性,并且更特定于抽象。预定义的异常非常通用,可以由许多不同的情况触发。编译器定义的异常不可移植,其含义可能会在同一个编译器的连续版本之间发生变化。这引入了太多不确定性,无法创建有用的处理程序。
如果您正在编写抽象,请记住用户不知道您在实现中使用的单元。这是信息隐藏的结果。如果您的抽象中引发了任何异常,您必须捕获并处理它。如果允许原始异常从您的抽象主体传播出去,用户将无法提供合理的处理程序。如果您自己的抽象无法有效地恢复,您仍然可以将异常转换为用户可以理解的形式。
转换异常意味着在原始异常的处理程序中引发用户定义的异常。这为单元用户引入了有意义的导出名称。一旦错误情况以应用程序的术语表达,就可以以这些术语进行处理。
- 将每个库单元包的规范放在与其主体不同的文件中。
- 避免定义不打算用作主程序的库单元子程序。如果定义了此类子程序,则为每个库单元子程序创建一个显式规范,在单独的文件中。
- 最小化子单元的使用。
- 优先使用子库单元,而不是子单元,将子系统结构化为易于管理的单元。
- 将每个子单元放在单独的文件中。
- 使用一致的文件命名约定。
- 优先使用私有子单元规范,而不是嵌套在包主体中,并使用它来扩展父单元的抽象或服务。
- 将私有子单元规范用于(其他)子单元需要的数据和子程序,这些子单元扩展了父单元的抽象或服务。
- 如果可能,通过编译器选项或其他不需要修改源代码的方法来表达配置选项。.
- 当配置编译指示必须放在源代码中时,考虑将它们隔离到每个分区的一个编译单元;如果指定,建议使用该分区的 main 子程序。
- 使用子程序来增强抽象。
- 将每个子程序限制为执行单个操作。
- 当子程序的主要目的是提供单个值时,使用函数。
- 尽量减少函数的副作用。
- 如果值不需要是静态的,请考虑使用无参数函数。
- 如果值应该被从类型派生的类型继承,请使用无参数函数(而不是常量)。
- 如果值本身可能会改变,请使用无参数函数。
- 使用包进行信息隐藏。
- 使用带有标记类型和私有类型的包来创建抽象数据类型。
- 使用包来模拟与问题领域相关的抽象实体。
- 使用包将相关的类型和对象声明分组在一起(例如,用于两个或多个库单元的通用声明)。
- 将机器依赖项封装在包中。将特定设备的软件接口放在包中,以方便更改到不同的设备。
- 将低级实现决策或接口放在包中的子程序中。
- 使用包和子程序来封装和隐藏可能更改的程序细节(Nissen 和 Wallis 1984)。
- 如果一个新的库单元代表了对原始抽象的逻辑扩展,请将其定义为子库单元。
- 如果一个新的库单元是独立的(例如,引入了仅部分依赖于现有抽象的新抽象),那么将新抽象封装在单独的库单元中。
- 使用子包来实现子系统。
- 对子系统中应该对子系统客户机可见的部分使用公共子单元。
- 对子系统中不应该对子系统客户机可见的部分使用私有子单元。
- 对仅在实现包规范中使用的局部声明使用私有子单元。
- 使用子包来实现构造函数,即使它们返回访问值。
- 使每个包都服务于单一目的。
- 使用包来分组相关数据、类型和子程序。
- 避免不相关的对象和子程序的集合(NASA 1987;Nissen 和 Wallis 1984)。
- 考虑对系统进行重构,将两个高度相关的单元移入同一个包(或包层次结构)中,或者将相对独立的单元移入单独的包中。
- 避免在包规范中声明变量。
- 使用任务来模拟问题域中的抽象、异步实体。
- 使用任务为多处理器体系结构定义并发算法。
- 使用任务执行并发、循环或优先级活动(NASA 1987)。
- 使用保护类型来控制或同步对数据或设备的访问。
- 使用保护类型来实现同步任务,例如被动资源监视器。
- 仅将包使用所需的內容放入其规范中。
- 将包规范中的声明数量降至最低。
- 不要仅仅因为构建简单而包含额外的操作。
- 将包规范中的上下文(with)子句降至最低。
- 重新考虑似乎需要大量参数的子程序。
- 不要仅仅为了限制参数数量而在子程序或包中操作全局数据。
- 避免不必要的可见性;将程序单元的实现细节隐藏在其用户之外。
- 使用子库单元来控制子系统接口部分的可见性。
- 对于那些不应该在子系统外部使用的声明,使用私有子包。
- 使用子库单元向不同的客户端展示实体的不同视图。
- 在确定了接口预期客户端的逻辑之后,设计(和重新设计)接口。
- 使用子包而不是嵌套包来呈现同一抽象的不同视图。
- 仅为了分组操作或隐藏公共实现细节,才将包规范嵌套在另一个包规范中。
- 考虑使用私有子包代替嵌套。
- 如果不能使用私有子包,则应尽可能地限制程序单元的可见性,方法是在包体中嵌套它们(Nissen 和 Wallis 1984)。
- 最大程度地减少在子程序和任务中嵌套程序单元。
- 最大程度地减少 with 子句适用的范围。
- 仅使用那些直接需要的单元。
- 仔细考虑任务的封装。
- 对于无法避免的内部错误,用户无法恢复,请声明一个用户可见的异常。在抽象内部,提供一种方法来区分不同的内部错误。
- 不要从其他上下文中借用异常名称。
- 导出(对用户可见地声明)所有可能引发的异常的名称。
- 在包中,记录每个子程序和任务条目可能引发哪些异常。
- 不要为可以在单元内避免或纠正的内部错误引发异常。
- 不要使用同一个异常来报告单元用户可以区分的不同类型的错误。
- 提供询问函数,允许单元用户避免引发异常。
- 如果可能,请在引发异常之前避免更改单元中的状态信息。
- 在最早的机会捕获和转换或处理所有预定义的和编译器定义的异常。
- 不要显式引发预定义的或实现定义的异常。
- 永远不要让异常传播到其范围之外。