面向对象设计到 Ada 的映射
1. 类关系与继承
在面向对象设计中,存在两种重要的关系:泛化 - 特化关系(也称为接口继承)和实现继承。泛化 - 特化关系源于系统对象模型,例如储蓄账户是账户的一种。而实现继承则是在审查类描述时,发现多个类的共同模式,将这些共同特征提取出来创建新的父类。
以下是一些关键类的定义:
-- 抽象类 Account
abstract class Account
attribute Number: Account_Number;
attribute Balance: Money;
attribute constant Owner: unbound shared Customer;
method Check Balance () : Money;
method Deposit_Cash (Amount: Money);
method Withdraw Cash (Amount: Money);
method Transfer (Dest: Account; Amount: Money);
method Owner Of () : Customer;
local method Deposit (Amount: Money);
local abstract method Withdraw (Amount: Money);
endclass
-- 类 Checking,继承自 Account
class Checking isa Account
attribute Credit Limit: Money;
local method Withdraw (Amount: Money);
-- Raises Overrun Error if withdrawing would exceed the credit limit.
endclass
-- 类 Savings,继承自 Account
class Savings isa Account
attribute Interest Rate: Rate;
local method Withdraw (Amount: Money);
-- Raises Overrun Error if withdrawing would result in an overdraft.
endclass
-- 类 Customer
class Customer
attribute Name: Name_Type;
attribute Address: Address_Type;
attribute constant Owned_Checking: bound shared Checking;
attribute constant Owned_Savings: bound shared Savings;
attribute Transactions: col Transaction;
method Checking_Account_Of () : Checking;
method Savings_Account_Of () : Savings;
method File (T: Transaction);
endclass
-- 类 Transaction
class Transaction
attribute Date: Date_Type;
attribute Amount: Money;
attribute constant Source: shared Account;
-- Source is undefined for a cash deposit.
attribute constant Dest: shared Account;
-- Dest is undefined for a cash withdrawal.
endclass
-- 类 Customer Services
class Customer Services
attribute constant Customers: col Customer;
method Generate_Monthly_Reports;
endclass
这些类之间的关系可以用以下表格总结:
| 类名 | 父类 | 关键属性 | 关键方法 |
| ---- | ---- | ---- | ---- |
| Account | 无 | Number, Balance, Owner | Check Balance, Deposit_Cash, Withdraw Cash, Transfer |
| Checking | Account | Credit Limit | Withdraw |
| Savings | Account | Interest Rate | Withdraw |
| Customer | 无 | Name, Address, Owned_Checking, Owned_Savings, Transactions | Checking_Account_Of, Savings_Account_Of, File |
| Transaction | 无 | Date, Amount, Source, Dest | 无 |
| Customer Services | 无 | Customers | Generate_Monthly_Reports |
2. 相互依赖关系
在设计阶段或开始实现时,需要关注类之间的相互依赖关系,并尽可能将它们解耦,主要有以下三个原因:
-
难以保证一致性
:类之间的关系最初在分析阶段的类模型中出现,之后会被“设计掉”,但对象属性和集合属性往往是这些关系的残留。独立的单向链接既不能确保一致性,也不能强制执行底层关系的基数约束。
-
难以维护
:包含许多相互依赖类的实现难以维护,因为要完全理解一个类,必须同时理解它所依赖的所有类,而且对一个类的更改可能会影响所有依赖类。
-
Ada 语言限制
:在 Ada 中,包规范之间不允许存在相互依赖关系。因此,相互依赖的类必须在同一个包规范中声明,这对物理模块化造成了阻碍。
要找出类之间的相互依赖关系,只需查看可见性和继承图中的循环。例如,在银行交易案例中,图中包含三个循环,其中一个是
Account - Customer - Transaction - Account
。虽然诊断相互依赖关系很容易,但处理它们却更困难。
对于相互依赖关系,有以下几种处理方法:
-
检查设计
:有些相互依赖关系是设计不佳的标志,特别是对象交互图中的数据流语义。如果可能,应修改它们以消除消息传递中的循环。
-
判断是否巧合
:有些相互依赖关系可能是巧合的,例如房子的主人是一个人,而一个人住在房子里,但这两个人可能没有关系。在这种情况下,链接可以暴露出来,因为它们是独立的。
-
追溯到类模型关系
:尽可能将相互依赖关系追溯到类模型中的关系,隐藏单向链接,并提供操作来创建、一致更新所有链接并导航它们。
-
其他方法
:如果上述方法都不起作用,可以保留相互依赖关系,或者添加一个纯人工类来打破它。
以下是一个简单的 mermaid 流程图,展示处理相互依赖关系的流程:
graph TD;
A[发现相互依赖关系] --> B{是否是设计问题};
B -- 是 --> C[修改设计];
B -- 否 --> D{是否是巧合};
D -- 是 --> E[暴露链接];
D -- 否 --> F[追溯到类模型关系];
F --> G[隐藏单向链接并提供操作];
C --> H{是否解决};
E --> H;
G --> H;
H -- 否 --> I[保留或添加人工类];
H -- 是 --> J[完成处理];
3. 类描述的映射
为了提供可能的未来“继承”,根类通常会在包中实现为标记类型。子类通过类型扩展实现,即从标记类型派生,通常包含在子包中。多个相关的类实现可能会包含在一个包中。
3.1 命名约定
一个类对应两个实现实体:一个包和一个类型。类的标识符不能同时命名包和类型,而且类的任何方法都会成为类型的原始操作,这意味着它有一个该类型的形式参数,也必须命名。我们将使用类名的复数形式作为包名,使用后缀作为类型名,变量、组件和参数可以直接使用“类”名。
3.2 抽象数据类型实现
通常,类应该实现为抽象数据类型(ADT),即标记类型或扩展部分是私有的。对于任何 ADT,必须为所有属性提供完整的构造函数、修改器和选择器操作。
3.3 属性映射
- 值属性 :例如账户的余额,成为标记类型或其私有视图的组件。它们可以是预定义或程序员定义的数据类型,有时值属性可以通过方法实现。
- 集合属性 :类似于值属性,集合本身通常是组件,而不是集合的访问值。
- 对象属性 :通过持有对象引用的组件实现,使用引用是为了保留对象的身份。为了满足可能的需求,标记类型声明通常会附带一个对类范围类型的通用访问类型。
3.4 方法映射
方法通常会映射到实现类的标记类型的原始操作,该标记类型成为操作的显式参数。与 C++ 或 Java 相比,Ada 的这种特性允许通过参数的模式指定对该参数的访问控制。本地方法(仅在类及其子类中可见)应映射到私有操作,即在包的私有部分声明。
3.5 其他映射情况
- 实现继承 :最好使用私有/隐藏继承,即父类型在包的私有部分派生。
- Mixin 类 :如果一个类是 mixin,在 Ada 中最好的做法是结合使用泛型实例化和类型派生。
- 常量对象属性 :有时可以映射到访问判别式,使用访问判别式的优点是组件始终是定义的,不会为空。
- 绑定对象属性 :可以通过受控访问值实现,控制访问值用于控制引用的服务器对象本身,并使其生命周期与包含对象的生命周期同步。
- 独占访问 :可以通过两种方式强制对服务器对象的独占访问:一是只有一条访问路径,且该路径隐藏在客户端对象的私有部分;二是使用受保护类型实现服务器类,但这样语言的内置继承功能将不可用,如有需要则必须使用一些变通方法。
以下是一个简单的代码示例,展示如何映射类描述:
-- 包 Accounts
package Accounts
is
type Account_Type is abstract tagged ...
end Accounts;
-- 子包 Accounts.Savings
package Accounts.Savings
is
type Savings_Account is new Account_Type with ...
end Accounts.Savings;
4. 集合映射
集合会根据所需的操作和性能要求映射到经典的数据结构,如集合、列表、队列、栈等。重要的是,对象集合应持有这些对象的引用,而不是对象本身,这是为了保留对象的身份。例如,与客户相关的交易是一个集合属性。
5. 方法体实现
方法体实现所需的大部分信息包含在对象交互图中,主要需要处理的问题是集合迭代和错误处理。异常用于错误处理,迭代可以通过以下几种常见的实现模式实现:
- 被动泛型迭代器
- 带有子程序访问参数的被动迭代器
- 主动迭代器
6. 银行交易案例映射
6.1 Person 类映射
package Persons is
type Name_Type...; type Address_Type...;
type Person_Type is abstract tagged limited
-- could be non limited, and even non abstract
record
Name: Name_Type;
Address: Address_Type;
end record;
type Person_Ref is access all Person_Type'Class;
end Persons;
6.2 Account 类映射
with Persons; use Persons;
package Accounts is
type Account_Number_Type is ...;
type Money_Type is ...; type Rate_Type is...;
type Account_Type is abstract tagged limited private;
procedure Create (Account: in out Account_Type; Owner: in Person_Ref);
-- Can be called only once. A check is made that Owner is not null,
-- and that s/he is in Customer'Class.
-- The account then gets a unique account number.
function Owner_Of (Account: Account_Type)return Person_Ref;
function Account_Number (Account: Account_Type) return Account_Number_Type;
function Check Balance (Account: Account_Type) return Money_Type;
procedure Deposit_Cash (Account: in out Account_Type; Amount: in Money_Type);
procedure Withdraw Cash (Account: in out Account_Type; Amount: in Money_Type);
procedure Transfer (Source: in out Account_Type; Dest: in out Account_Type'Class; Amount: in Money_Type);
-- Dest must not be limited to the specific type!
Overrun Error: exception;
private
procedure Deposit (Account: in out Account_Type; Amount: in Money_Type);
procedure Withdraw (Account: in out Account_Type; Amount: in Money_Type) is abstract;
-- Raises Overrun Error if withdrawing is impossible.
type Account_Type is abstract tagged limited
record
Owner: Person Ref;
Number : Account_Number_Type := 0;
Balance: Money_Type := 0.0;
end record;
end Accounts;
6.3 Checking 类映射
package Accounts.Checking is
type Checking_Type is new Account_Type with private;
type Checking_Ref is access all Checking_Type'Class;
procedure Set Credit Limit (Account: in out Checking_Type; Amount: in Money_Type);
function Credit Limit Of (Account: Checking_Type) return Money_Type;
Overrun_Error: exception renames Accounts.Overrun_Error;
private
procedure Withdraw (Account: in out Checking_Type; Amount: in Money_Type);
-- Raises Overrun Error if withdrawing would exceed the credit limit.
type Checking_Type is new Account_Type with record
Credit_Limit: Money_Type := 0.0;
end record;
end Accounts.Checking;
6.4 Transaction 类映射
with Accounts; use Accounts;
package Transactions is
subtype Date_Type is Ada. Calendar.Time;
type Transaction_Type(Source: in Account_Ref; Dest: in Account_Ref) is tagged limited private;
type Transaction_Ref is access all Transaction_Type'Class;
procedure Create (Transaction: out Transaction_Type; Amount: in Money_Type);
-- The transaction is timestamped with the help of the clock.
function Date Of (Transaction: Transaction_Type) return Date_Type;
function Amount Of (Transaction: Transaction_Type) return Money_Type;
function Source Of (Transaction: Transaction_Type) return Account_Ref;
function Dest Of (Transaction: Transaction_Type) return Account_Ref;
private
type Transaction_Type (Source: in Account_Ref; Dest: in Account_Ref) is tagged limited
record
Date: Date_Type := Ada. Calendar.Cleck;
Amount: Money_Type;
end record;
end Transactions;
6.5 Customer 类映射
with Sets_G; with Transactions; use Transactions;
package Transaction_Sets is new Sets_G (Transaction_Ref);
with Ada. Finalization; use Ada. Finalization;
with Persons; use Persons;
with Accounts.Savings; use Accounts.Savings;
with Accounts.Checking; use Accounts.Checking;
with Transactions; use Transactions; with Transaction_Sets;
package Customers is
-- 此处代码未完整给出
6.6 Customer Services 类映射
由于系统中只有一个
Customer Services
对象,因此可以使用带有状态的包(即抽象状态机)而不是类型来实现它,其映射过程相对直接,具体代码未展示。
以下是一个表格,总结银行交易案例中各个类的映射情况:
| 类名 | 映射类型 | 关键代码 |
| ---- | ---- | ---- |
| Person | 开放类型 |
package Persons
中的相关代码 |
| Account | 抽象数据类型 |
package Accounts
中的相关代码 |
| Checking | 从 Account 派生 |
package Accounts.Checking
中的相关代码 |
| Transaction | 使用判别式和返回对象访问值的函数 |
package Transactions
中的相关代码 |
| Customer | 包含集合属性,控制账户生命周期 |
package Customers
中的相关代码 |
| Customer Services | 抽象状态机 | 未展示具体代码 |
通过以上对银行交易案例的映射,我们可以看到如何将面向对象的设计准确地转换为 Ada 代码,同时遵循前面提到的各种原则和方法,包括类关系处理、相互依赖关系解耦、类描述映射、集合映射以及方法体实现等方面的内容。这样的映射过程有助于提高代码的可维护性、可扩展性和一致性,同时充分利用 Ada 语言的特性。
7. 银行交易案例映射的详细分析
7.1 Person 类
Person
类被映射为一个“开放”类型,在
Persons
包中定义。它包含基本的个人信息,如姓名和地址。以下是其代码结构:
package Persons is
type Name_Type...; type Address_Type...;
type Person_Type is abstract tagged limited
-- could be non limited, and even non abstract
record
Name: Name_Type;
Address: Address_Type;
end record;
type Person_Ref is access all Person_Type'Class;
end Persons;
这个类的设计允许在不同的场景中灵活使用,既可以作为抽象类型,也可以根据需要调整为非抽象和非受限类型。
7.2 Account 类
Account
类是一个抽象数据类型,在
Accounts
包中实现。它具有账户的基本属性,如账户号码、余额和所有者。以下是其详细代码:
with Persons; use Persons;
package Accounts is
type Account_Number_Type is ...;
type Money_Type is ...; type Rate_Type is...;
type Account_Type is abstract tagged limited private;
procedure Create (Account: in out Account_Type; Owner: in Person_Ref);
-- Can be called only once. A check is made that Owner is not null,
-- and that s/he is in Customer'Class.
-- The account then gets a unique account number.
function Owner_Of (Account: Account_Type)return Person_Ref;
function Account_Number (Account: Account_Type) return Account_Number_Type;
function Check Balance (Account: Account_Type) return Money_Type;
procedure Deposit_Cash (Account: in out Account_Type; Amount: in Money_Type);
procedure Withdraw Cash (Account: in out Account_Type; Amount: in Money_Type);
procedure Transfer (Source: in out Account_Type; Dest: in out Account_Type'Class; Amount: in Money_Type);
-- Dest must not be limited to the specific type!
Overrun Error: exception;
private
procedure Deposit (Account: in out Account_Type; Amount: in Money_Type);
procedure Withdraw (Account: in out Account_Type; Amount: in Money_Type) is abstract;
-- Raises Overrun Error if withdrawing is impossible.
type Account_Type is abstract tagged limited
record
Owner: Person Ref;
Number : Account_Number_Type := 0;
Balance: Money_Type := 0.0;
end record;
end Accounts;
Create
过程用于创建账户,会进行必要的检查,确保所有者不为空且属于
Customer
类,并为账户分配唯一的账户号码。
Deposit_Cash
和
Withdraw Cash
方法用于处理现金的存入和取出,
Transfer
方法用于在不同账户之间进行转账。
7.3 Checking 类
Checking
类继承自
Account
类,在
Accounts.Checking
子包中定义。它增加了信用额度的属性,以下是其代码:
package Accounts.Checking is
type Checking_Type is new Account_Type with private;
type Checking_Ref is access all Checking_Type'Class;
procedure Set Credit Limit (Account: in out Checking_Type; Amount: in Money_Type);
function Credit Limit Of (Account: Checking_Type) return Money_Type;
Overrun_Error: exception renames Accounts.Overrun_Error;
private
procedure Withdraw (Account: in out Checking_Type; Amount: in Money_Type);
-- Raises Overrun Error if withdrawing would exceed the credit limit.
type Checking_Type is new Account_Type with record
Credit_Limit: Money_Type := 0.0;
end record;
end Accounts.Checking;
Set Credit Limit
方法用于设置信用额度,
Credit Limit Of
方法用于获取当前的信用额度。在进行取款操作时,如果超过信用额度,会抛出
Overrun_Error
异常。
7.4 Transaction 类
Transaction
类在
Transactions
包中定义,使用判别式来处理常量对象属性。以下是其代码:
with Accounts; use Accounts;
package Transactions is
subtype Date_Type is Ada. Calendar.Time;
type Transaction_Type(Source: in Account_Ref; Dest: in Account_Ref) is tagged limited private;
type Transaction_Ref is access all Transaction_Type'Class;
procedure Create (Transaction: out Transaction_Type; Amount: in Money_Type);
-- The transaction is timestamped with the help of the clock.
function Date Of (Transaction: Transaction_Type) return Date_Type;
function Amount Of (Transaction: Transaction_Type) return Money_Type;
function Source Of (Transaction: Transaction_Type) return Account_Ref;
function Dest Of (Transaction: Transaction_Type) return Account_Ref;
private
type Transaction_Type (Source: in Account_Ref; Dest: in Account_Ref) is tagged limited
record
Date: Date_Type := Ada. Calendar.Cleck;
Amount: Money_Type;
end record;
end Transactions;
Create
过程用于创建交易记录,并使用时钟为交易添加时间戳。
Date Of
、
Amount Of
、
Source Of
和
Dest Of
方法分别用于获取交易的日期、金额、源账户和目标账户。
7.5 Customer 类
Customer
类在
Customers
包中定义,包含集合属性
Transactions
,并控制其拥有的账户的生命周期。以下是其部分代码:
with Sets_G; with Transactions; use Transactions;
package Transaction_Sets is new Sets_G (Transaction_Ref);
with Ada. Finalization; use Ada. Finalization;
with Persons; use Persons;
with Accounts.Savings; use Accounts.Savings;
with Accounts.Checking; use Accounts.Checking;
with Transactions; use Transactions; with Transaction_Sets;
package Customers is
-- 此处代码未完整给出
Customer
类通过封装账户的引用在受控类型中,确保账户不会比其所有者的生命周期更长。
7.6 Customer Services 类
由于系统中只有一个
Customer Services
对象,使用带有状态的包(抽象状态机)来实现它。其映射过程相对直接,虽然具体代码未展示,但主要功能是生成月度报告。
以下是一个 mermaid 流程图,展示银行交易的主要流程:
graph LR;
A[客户] --> B[账户操作];
B --> C{操作类型};
C -- 存款 --> D[Deposit_Cash];
C -- 取款 --> E[Withdraw Cash];
C -- 转账 --> F[Transfer];
D --> G[更新账户余额];
E --> H{是否超限额};
H -- 是 --> I[抛出 Overrun Error];
H -- 否 --> G;
F --> J[更新源账户和目标账户余额];
K[交易记录] --> L[Transaction 类];
M[客户服务] --> N[Generate_Monthly_Reports];
8. 总结与建议
8.1 总结
通过以上对面向对象设计到 Ada 代码的映射过程的详细介绍,我们可以看到在处理类关系、相互依赖关系、类描述映射、集合映射以及方法体实现等方面的重要原则和方法。在银行交易案例中,我们展示了如何将各个类准确地转换为 Ada 代码,同时遵循了提高代码可维护性、可扩展性和一致性的目标。
8.2 建议
- 设计阶段 :在设计类时,要充分考虑类之间的相互依赖关系,尽量减少循环依赖,提高代码的模块化程度。对于可能出现的相互依赖关系,提前规划好解耦的方法。
- 命名约定 :遵循统一的命名约定,确保代码的可读性和可维护性。使用类名的复数形式作为包名,后缀作为类型名,变量、组件和参数使用“类”名,保持项目内的一致性。
- 代码实现 :在实现类时,将类作为抽象数据类型,提供完整的构造函数、修改器和选择器操作。对于集合属性,使用引用而不是对象本身,以保留对象的身份。在处理方法体时,利用对象交互图中的信息,选择合适的迭代模式和错误处理方式。
以下是一个表格,总结不同类的特点和建议:
| 类名 | 特点 | 建议 |
| ---- | ---- | ---- |
| Person | 开放类型,包含基本个人信息 | 可根据具体需求调整为非抽象和非受限类型 |
| Account | 抽象数据类型,账户基本属性和操作 | 确保
Create
方法的检查逻辑正确,处理好异常情况 |
| Checking | 继承自 Account,增加信用额度属性 | 注意取款时的信用额度检查,合理处理异常 |
| Transaction | 使用判别式和返回对象访问值的函数 | 确保交易记录的时间戳准确,各方法功能正确 |
| Customer | 包含集合属性,控制账户生命周期 | 封装账户引用,确保账户与客户生命周期同步 |
| Customer Services | 抽象状态机,生成月度报告 | 确保报告生成逻辑正确,提高性能 |
通过遵循这些原则和建议,可以更好地将面向对象的设计转换为高质量的 Ada 代码,充分发挥 Ada 语言的优势。