Appendix A: Hands-On Object-Oriented Programming
The goal of this appendix is to present a hands-on view on how to translate a system from C to Ada and improve it with object-oriented programming.
System Overview
Let's start with an overview of a simple system that we'll implement and use below. The main system is called AB and it combines two systems A and B. System AB is not supposed to do anything useful. However, it can serve as a good model for the hands-on we're about to start.
This is a list of requirements for the individual systems A and B, and the combined system AB:
System A:
The system can be activated and deactivated.
During activation, the system's values are reset.
Its current value (in floating-point) can be retrieved.
This value is the average of the two internal floating-point values.
Its current state (activated or deactivated) can be retrieved.
System B:
The system can be activated and deactivated.
During activation, the system's value is reset.
Its current value (in floating-point) can be retrieved.
Its current state (activated or deactivated) can be retrieved.
System AB
The system contains an instance of system A and an instance of system B.
The system can be activated and deactivated.
System AB activates both systems A and B during its own activation.
System AB deactivates both systems A and B during its own deactivation.
Its current value (in floating-point) can be retrieved.
This value is the average of the current values of systems A and B.
Its current state (activated or deactivated) can be retrieved.
AB is only considered activated when both systems A and B are activated.
The system's health can be checked.
This check consists in calculating the absolute difference D between the current values of systems A and B and checking whether D is below a threshold of 0.1.
The source-code in the following section contains an implementation of these requirements.
Non Object-Oriented Approach
In this section, we look into implementations (in both C and Ada) of system AB that don't make use of object-oriented programming.
Starting point in C
Let's start with an implementation in C for the system described above:
[C]
typedef struct { float val[2]; int active; } A; void A_activate (A *a); int A_is_active (A *a); float A_value (A *a); void A_deactivate (A *a);#include "system_a.h" void A_activate (A *a) { int i; for (i = 0; i < 2; i++) { a->val[i] = 0.0; } a->active = 1; } int A_is_active (A *a) { return a->active == 1; } float A_value (A *a) { return (a->val[0] + a->val[1]) / 2.0; } void A_deactivate (A *a) { a->active = 0; }typedef struct { float val; int active; } B; void B_activate (B *b); int B_is_active (B *b); float B_value (B *b); void B_deactivate (B *b);#include "system_b.h" void B_activate (B *b) { b->val = 0.0; b->active = 1; } int B_is_active (B *b) { return b->active == 1; } float B_value (B *b) { return b->val; } void B_deactivate (B *b) { b->active = 0; }#include "system_a.h" #include "system_b.h" typedef struct { A a; B b; } AB; void AB_activate (AB *ab); int AB_is_active (AB *ab); float AB_value (AB *ab); int AB_check (AB *ab); void AB_deactivate (AB *ab);#include <math.h> #include "system_ab.h" void AB_activate (AB *ab) { A_activate (&ab->a); B_activate (&ab->b); } int AB_is_active (AB *ab) { return A_is_active(&ab->a) && B_is_active(&ab->b); } float AB_value (AB *ab) { return (A_value (&ab->a) + B_value (&ab->b)) / 2; } int AB_check (AB *ab) { const float threshold = 0.1; return fabs (A_value (&ab->a) - B_value (&ab->b)) < threshold; } void AB_deactivate (AB *ab) { A_deactivate (&ab->a); B_deactivate (&ab->b); }#include <stdio.h> #include "system_ab.h" void display_active (AB *ab) { if (AB_is_active (ab)) printf ("System AB is active.\n"); else printf ("System AB is not active.\n"); } void display_check (AB *ab) { if (AB_check (ab)) printf ("System AB check: PASSED.\n"); else printf ("System AB check: FAILED.\n"); } int main() { AB s; printf ("Activating system AB...\n"); AB_activate (&s); display_active (&s); display_check (&s); printf ("Deactivating system AB...\n"); AB_deactivate (&s); display_active (&s); }
Here, each system is implemented in a separate set of header and source-code
files. For example, the API of system AB is in system_ab.h
and its
implementation in system_ab.c
.
In the main application, we instantiate system AB and activate it. Then, we proceed to display the activation state and the result of the system's health check. Finally, we deactivate the system and display the activation state again.
Initial translation to Ada
The direct implementation in Ada is:
[Ada]
package System_A is type Val_Array is array (Positive range <>) of Float; type A is record Val : Val_Array (1 .. 2); Active : Boolean; end record; procedure A_Activate (E : in out A); function A_Is_Active (E : A) return Boolean; function A_Value (E : A) return Float; procedure A_Deactivate (E : in out A); end System_A;package body System_A is procedure A_Activate (E : in out A) is begin E.Val := (others => 0.0); E.Active := True; end A_Activate; function A_Is_Active (E : A) return Boolean is begin return E.Active; end A_Is_Active; function A_Value (E : A) return Float is begin return (E.Val (1) + E.Val (2)) / 2.0; end A_Value; procedure A_Deactivate (E : in out A) is begin E.Active := False; end A_Deactivate; end System_A;package System_B is type B is record Val : Float; Active : Boolean; end record; procedure B_Activate (E : in out B); function B_Is_Active (E : B) return Boolean; function B_Value (E : B) return Float; procedure B_Deactivate (E : in out B); end System_B;package body System_B is procedure B_Activate (E : in out B) is begin E.Val := 0.0; E.Active := True; end B_Activate; function B_Is_Active (E : B) return Boolean is begin return E.Active; end B_Is_Active; function B_Value (E : B) return Float is begin return E.Val; end B_Value; procedure B_Deactivate (E : in out B) is begin E.Active := False; end B_Deactivate; end System_B;with System_A; use System_A; with System_B; use System_B; package System_AB is type AB is record SA : A; SB : B; end record; procedure AB_Activate (E : in out AB); function AB_Is_Active (E : AB) return Boolean; function AB_Value (E : AB) return Float; function AB_Check (E : AB) return Boolean; procedure AB_Deactivate (E : in out AB); end System_AB;package body System_AB is procedure AB_Activate (E : in out AB) is begin A_Activate (E.SA); B_Activate (E.SB); end AB_Activate; function AB_Is_Active (E : AB) return Boolean is begin return A_Is_Active (E.SA) and B_Is_Active (E.SB); end AB_Is_Active; function AB_Value (E : AB) return Float is begin return (A_Value (E.SA) + B_Value (E.SB)) / 2.0; end AB_Value; function AB_Check (E : AB) return Boolean is Threshold : constant := 0.1; begin return abs (A_Value (E.SA) - B_Value (E.SB)) < Threshold; end AB_Check; procedure AB_Deactivate (E : in out AB) is begin A_Deactivate (E.SA); B_Deactivate (E.SB); end AB_Deactivate; end System_AB;with Ada.Text_IO; use Ada.Text_IO; with System_AB; use System_AB; procedure Main is procedure Display_Active (E : AB) is begin if AB_Is_Active (E) then Put_Line ("System AB is active"); else Put_Line ("System AB is not active"); end if; end Display_Active; procedure Display_Check (E : AB) is begin if AB_Check (E) then Put_Line ("System AB check: PASSED"); else Put_Line ("System AB check: FAILED"); end if; end Display_Check; S : AB; begin Put_Line ("Activating system AB..."); AB_Activate (S); Display_Active (S); Display_Check (S); Put_Line ("Deactivating system AB..."); AB_Deactivate (S); Display_Active (S); end Main;
As you can see, this is a direct translation that doesn't change much of the structure of the original C code. Here, the goal was to simply translate the system from one language to another and make sure that the behavior remains the same.
Improved Ada implementation
By analyzing this direct implementation, we may notice the following points:
Packages
System_A
,System_B
andSystem_AB
are used to describe aspects of the same system. Instead of having three distinct packages, we could group them as child packages of a common parent package — let's call itSimple
, since this system is supposed to be simple. This approach has the advantage of allowing us to later use the parent package to implement functionality that is common for all parts of the system.Since we have subprograms that operate on types
A
,B
andAB
, we should avoid exposing the record components by moving the type declarations to the private part of the corresponding packages.Since Ada supports subprogram overloading — as discussed in this section from chapter 2 —, we don't need to have different names for subprograms with similar functionality. For example, instead of having
A_Is_Active
andB_Is_Active
, we can simply name these functionsIs_Active
for both typesA
andB
.Some of the functions — such as
A_Is_Active
andA_Value
— are very simple, so we could simplify them with expression functions.
This is an update to the implementation that addresses all the points above:
[Ada]
package Simple with Pure is end Simple;package Simple.System_A is type A is private; procedure Activate (E : in out A); function Is_Active (E : A) return Boolean; function Value (E : A) return Float; procedure Finalize (E : in out A); private type Val_Array is array (Positive range <>) of Float; type A is record Val : Val_Array (1 .. 2); Active : Boolean; end record; end Simple.System_A;package body Simple.System_A is procedure Activate (E : in out A) is begin E.Val := (others => 0.0); E.Active := True; end Activate; function Is_Active (E : A) return Boolean is (E.Active); function Value (E : A) return Float is begin return (E.Val (1) + E.Val (2)) / 2.0; end Value; procedure Finalize (E : in out A) is begin E.Active := False; end Finalize; end Simple.System_A;package Simple.System_B is type B is private; procedure Activate (E : in out B); function Is_Active (E : B) return Boolean; function Value (E : B) return Float; procedure Finalize (E : in out B); private type B is record Val : Float; Active : Boolean; end record; end Simple.System_B;package body Simple.System_B is procedure Activate (E : in out B) is begin E.Val := 0.0; E.Active := True; end Activate; function Is_Active (E : B) return Boolean is begin return E.Active; end Is_Active; function Value (E : B) return Float is (E.Val); procedure Finalize (E : in out B) is begin E.Active := False; end Finalize; end Simple.System_B;with Simple.System_A; use Simple.System_A; with Simple.System_B; use Simple.System_B; package Simple.System_AB is type AB is private; procedure Activate (E : in out AB); function Is_Active (E : AB) return Boolean; function Value (E : AB) return Float; function Check (E : AB) return Boolean; procedure Finalize (E : in out AB); private type AB is record SA : A; SB : B; end record; end Simple.System_AB;package body Simple.System_AB is procedure Activate (E : in out AB) is begin Activate (E.SA); Activate (E.SB); end Activate; function Is_Active (E : AB) return Boolean is (Is_Active (E.SA) and Is_Active (E.SB)); function Value (E : AB) return Float is ((Value (E.SA) + Value (E.SB)) / 2.0); function Check (E : AB) return Boolean is Threshold : constant := 0.1; begin return abs (Value (E.SA) - Value (E.SB)) < Threshold; end Check; procedure Finalize (E : in out AB) is begin Finalize (E.SA); Finalize (E.SB); end Finalize; end Simple.System_AB;with Ada.Text_IO; use Ada.Text_IO; with Simple.System_AB; use Simple.System_AB; procedure Main is procedure Display_Active (E : AB) is begin if Is_Active (E) then Put_Line ("System AB is active"); else Put_Line ("System AB is not active"); end if; end Display_Active; procedure Display_Check (E : AB) is begin if Check (E) then Put_Line ("System AB check: PASSED"); else Put_Line ("System AB check: FAILED"); end if; end Display_Check; S : AB; begin Put_Line ("Activating system AB..."); Activate (S); Display_Active (S); Display_Check (S); Put_Line ("Deactivating system AB..."); Finalize (S); Display_Active (S); end Main;
First Object-Oriented Approach
Until now, we haven't used any of the object-oriented programming features of the Ada language. So we can start by analyzing the API of systems A and B and deciding how to best abstract some of its elements using object-oriented programming.
Interfaces
The first thing we may notice is that we actually have two distinct sets of APIs there:
one API for activating and deactivating the system.
one API for retrieving the value of the system.
We can use this distinction to declare two interface types:
Activation_IF
for theActivate
andDeactivate
procedures and theIs_Active
function;Value_Retrieval_IF
for theValue
function.
This is how the declaration could look like:
type Activation_IF is interface;
procedure Activate (E : in out Activation_IF) is abstract;
function Is_Active (E : Activation_IF) return Boolean is abstract;
procedure Deactivate (E : in out Activation_IF) is abstract;
type Value_Retrieval_IF is interface;
function Value (E : Value_Retrieval_IF) return Float is abstract;
Note that, because we are declaring interface types, all operations on those
types must be abstract or, in the case of procedures, they can also be declared
null
. For example, we could change the declaration of the procedures
above to this:
procedure Activate (E : in out Activation_IF) is null;
procedure Deactivate (E : in out Activation_IF) is null;
When an operation is declared abstract, we must override it for the type that
derives from the interface. When a procedure is declared null
, it acts
as a do-nothing default. In this case, overriding the operation is optional for
the type that derives from this interface.
Base type
Since the original system needs both interfaces we've just described, we have
to declare another type that combines those interfaces. We can do this by
declaring the interface type Sys_Base
, which serves as the base type for
systems A and B. This is the declaration:
type Sys_Base is interface and Activation_IF and Value_Retrieval_IF;
Since the system activation functionality is common for both systems A and B,
we could implement it as part of Sys_Base
. That would require changing
the declaration from a simple interface to an abstract record:
type Sys_Base is abstract new Activation_IF and Value_Retrieval_IF
with null record;
Now, we can add the Boolean component to the record (as a private component)
and override the subprograms of the Activation_IF
interface. This is the
adapted declaration:
type Sys_Base is abstract new Activation_IF and Value_Retrieval_IF with private;
overriding procedure Activate (E : in out Sys_Base);
overriding function Is_Active (E : Sys_Base) return Boolean;
overriding procedure Deactivate (E : in out Sys_Base);
private
type Sys_Base is abstract new Activation_IF and Value_Retrieval_IF with record
Active : Boolean;
end record;
Derived types
In the declaration of the Sys_Base
type we've just seen, we're not
overriding the Value
function — from the
Value_Retrieval_IF
interface — for the Sys_Base
type, so it
remains an abstract function for Sys_Base
. Therefore, the
Sys_Base
type itself remains abstract and needs be explicitly declared
as such.
We use this strategy to ensure that all types derived from Sys_Base
need
to implement their own version of the Value
function. For example:
type A is new Sys_Base with private;
overriding function Value (E : A) return Float;
Here, the A
type is derived from the Sys_Base
and it includes its
own version of the Value
function by overriding it. Therefore,
A
is not an abstract type anymore and can be used to declare objects:
procedure Main is
Obj : A;
V : Float;
begin
Obj.Activate;
V := Obj.Value;
end Main;
Important
Note that the use of the overriding
keyword in the subprogram
declaration is not strictly necessary. In fact, we could leave this keyword
out, and the code would still compile. However, if provided, the compiler
will check whether the information is correct.
Using the overriding
keyword can help to avoid bad surprises
— when you may think that you're overriding a subprogram, but
you're actually not. Similarly, you can also write not overriding
to
be explicit about subprograms that are new primitives of a derived type.
For example:
not overriding function Check (E : AB) return Boolean;
We also need to declare the values that are used internally in systems A and B. For system A, this is the declaration:
type A is new Sys_Base with private;
overriding function Value (E : A) return Float;
private
type Val_Array is array (Positive range <>) of Float;
type A is new Sys_Base with record
Val : Val_Array (1 .. 2);
end record;
Subprograms from parent
In the previous implementation, we've seen that the A_Activate
and
B_Activate
procedures perform the following steps:
initialize internal values;
indicate that the system is active (by setting the
Active
flag toTrue
).
In the implementation of the Activate
procedure for the Sys_Base
type, however, we're only dealing with the second step. Therefore, we need to
override the Activate
procedure and make sure that we initialize
internal values as well. First, we need to declare this procedure for type
A
:
type A is new Sys_Base with private;
overriding procedure Activate (E : in out A);
In the implementation of Activate
, we should call the Activate
procedure from the parent (Sys_Base
) to ensure that whatever was
performed for the parent will be performed in the derived type as well. For
example:
overriding procedure Activate (E : in out A) is
begin
E.Val := (others => 0.0);
Sys_Base (E).Activate; -- Calling Activate for Sys_Base type:
-- this call initializes the Active flag.
end;
Here, by writing Sys_Base (E)
, we're performing a view conversion.
Basically, we're telling the compiler to view E
not as an object of
type A
, but of type Sys_Base
. When we do this, any operation
performed on this object will be done as if it was an object of
Sys_Base
type, which includes calling the Activate
procedure of
the Sys_Base
type.
Important
If we write T (Obj).Proc
, we're telling the compiler to call the
Proc
procedure of type T
and apply it on Obj
.
If we write T'Class (Obj).Proc
, however, we're telling the compiler
to dispatch the call. For example, if Obj
is of derived type
T2
and there's an overridden Proc
procedure for type
T2
, then this procedure will be called instead of the Proc
procedure for type T
.
Type AB
While the implementation of systems A and B is almost straightforward, it gets
more interesting in the case of system AB. Here, we have a similar API, but we
don't need the activation mechanism implemented in the abstract type
Sys_Base
. Therefore, deriving from Sys_Base
is not the best
option. Instead, when declaring the AB
type, we can simply use the same
interfaces as we did for Sys_Base
, but keep it independent from
Sys_Base
. For example:
type AB is new Activation_IF and Value_Retrieval_IF with private;
private
type AB is new Activation_IF and Value_Retrieval_IF with record
SA : A;
SB : B;
end record;
Naturally, we still need to override all the subprograms that are part of the
Activation_IF
and Value_Retrieval_IF
interfaces. Also, we need
to implement the additional Check
function that was originally only
available on system AB. Therefore, we declare these subprograms:
overriding procedure Activate (E : in out AB);
overriding function Is_Active (E : AB) return Boolean;
overriding procedure Deactivate (E : in out AB);
overriding function Value (E : AB) return Float;
not overriding function Check (E : AB) return Boolean;
Updated source-code
Finally, this is the complete source-code example:
[Ada]
package Simple is type Activation_IF is interface; procedure Activate (E : in out Activation_IF) is abstract; function Is_Active (E : Activation_IF) return Boolean is abstract; procedure Deactivate (E : in out Activation_IF) is abstract; type Value_Retrieval_IF is interface; function Value (E : Value_Retrieval_IF) return Float is abstract; type Sys_Base is abstract new Activation_IF and Value_Retrieval_IF with private; overriding procedure Activate (E : in out Sys_Base); overriding function Is_Active (E : Sys_Base) return Boolean; overriding procedure Deactivate (E : in out Sys_Base); private type Sys_Base is abstract new Activation_IF and Value_Retrieval_IF with record Active : Boolean; end record; end Simple;package body Simple is overriding procedure Activate (E : in out Sys_Base) is begin E.Active := True; end Activate; overriding function Is_Active (E : Sys_Base) return Boolean is (E.Active); overriding procedure Deactivate (E : in out Sys_Base) is begin E.Active := False; end Deactivate; end Simple;package Simple.System_A is type A is new Sys_Base with private; overriding procedure Activate (E : in out A); overriding function Value (E : A) return Float; private type Val_Array is array (Positive range <>) of Float; type A is new Sys_Base with record Val : Val_Array (1 .. 2); end record; end Simple.System_A;package body Simple.System_A is procedure Activate (E : in out A) is begin E.Val := (others => 0.0); Sys_Base (E).Activate; end Activate; function Value (E : A) return Float is pragma Assert (E.Val'Length = 2); begin return (E.Val (1) + E.Val (2)) / 2.0; end Value; end Simple.System_A;package Simple.System_B is type B is new Sys_Base with private; overriding procedure Activate (E : in out B); overriding function Value (E : B) return Float; private type B is new Sys_Base with record Val : Float; end record; end Simple.System_B;package body Simple.System_B is procedure Activate (E : in out B) is begin E.Val := 0.0; Sys_Base (E).Activate; end Activate; function Value (E : B) return Float is (E.Val); end Simple.System_B;with Simple.System_A; use Simple.System_A; with Simple.System_B; use Simple.System_B; package Simple.System_AB is type AB is new Activation_IF and Value_Retrieval_IF with private; overriding procedure Activate (E : in out AB); overriding function Is_Active (E : AB) return Boolean; overriding procedure Deactivate (E : in out AB); overriding function Value (E : AB) return Float; not overriding function Check (E : AB) return Boolean; private type AB is new Activation_IF and Value_Retrieval_IF with record SA : A; SB : B; end record; end Simple.System_AB;package body Simple.System_AB is procedure Activate (E : in out AB) is begin E.SA.Activate; E.SB.Activate; end Activate; function Is_Active (E : AB) return Boolean is (E.SA.Is_Active and E.SB.Is_Active); procedure Deactivate (E : in out AB) is begin E.SA.Deactivate; E.SB.Deactivate; end Deactivate; function Value (E : AB) return Float is ((E.SA.Value + E.SB.Value) / 2.0); function Check (E : AB) return Boolean is Threshold : constant := 0.1; begin return abs (E.SA.Value - E.SB.Value) < Threshold; end Check; end Simple.System_AB;with Ada.Text_IO; use Ada.Text_IO; with Simple.System_AB; use Simple.System_AB; procedure Main is procedure Display_Active (E : AB) is begin if Is_Active (E) then Put_Line ("System AB is active"); else Put_Line ("System AB is not active"); end if; end Display_Active; procedure Display_Check (E : AB) is begin if Check (E) then Put_Line ("System AB check: PASSED"); else Put_Line ("System AB check: FAILED"); end if; end Display_Check; S : AB; begin Put_Line ("Activating system AB..."); Activate (S); Display_Active (S); Display_Check (S); Put_Line ("Deactivating system AB..."); Deactivate (S); Display_Active (S); end Main;
Further Improvements
When analyzing the complete source-code, we see that there are at least two areas that we could still improve.
Dispatching calls
The first issue concerns the implementation of the Activate
procedure
for types derived from Sys_Base
. For those derived types, we're
expecting that the Activate
procedure of the parent must be called in
the implementation of the overriding Activate
procedure. For example:
package body Simple.System_A is
procedure Activate (E : in out A) is
begin
E.Val := (others => 0.0);
Activate (Sys_Base (E));
end;
If a developer forgets to call that specific Activate
procedure,
however, the system won't work as expected. A better strategy could be the
following:
Declare a new
Activation_Reset
procedure forSys_Base
type.Make a dispatching call to the
Activation_Reset
procedure in the body of theActivate
procedure (of theSys_Base
type).Let the derived types implement their own version of the
Activation_Reset
procedure.
This is a simplified view of the implementation using the points described above:
package Simple is
type Sys_Base is abstract new Activation_IF and Value_Retrieval_IF with
private;
not overriding procedure Activation_Reset (E : in out Sys_Base) is abstract;
end Simple;
package body Simple is
procedure Activate (E : in out Sys_Base) is
begin
-- NOTE: calling "E.Activation_Reset" does NOT dispatch!
-- We need to use the 'Class attribute here --- not using this
-- attribute is an error that will be caught by the compiler.
Sys_Base'Class (E).Activation_Reset;
E.Active := True;
end Activate;
end Simple;
package Simple.System_A is
type A is new Sys_Base with private;
private
type Val_Array is array (Positive range <>) of Float;
type A is new Sys_Base with record
Val : Val_Array (1 .. 2);
end record;
overriding procedure Activation_Reset (E : in out A);
end Simple.System_A;
package body Simple.System_A is
procedure Activation_Reset (E : in out A) is
begin
E.Val := (others => 0.0);
end Activation_Reset;
end Simple.System_A;
An important detail is that, in the implementation of Activate
, we use
Sys_Base'Class
to ensure that the call to Activation_Reset
will
dispatch. If we had just written E.Activation_Reset
instead, then we
would be calling the Activation_Reset
procedure of Sys_Base
itself, which is not what we actually want here. The compiler will catch the
error if you don't do the conversion to the class-wide type, because it would
otherwise be a statically-bound call to an abstract procedure, which is illegal
at compile-time.
Dynamic allocation
The next area that we could improve is in the declaration of the system AB. In
the previous implementation, we were explicitly describing the two
components of that system, namely a component of type A
and a component
of type B
:
type AB is new Activation_IF and Value_Retrieval_IF with record
SA : A;
SB : B;
end record;
Of course, this declaration matches the system requirements that we presented
in the beginning. However, we could use strategies that make it easier to
incorporate requirement changes later on. For example, we could hide this
information about systems A and B by simply declaring an array of
components of type access Sys_Base'Class
and allocate them dynamically
in the body of the package. Naturally, this approach might not be suitable for
certain platforms. However, the advantage would be that, if we wanted to
replace the component of type B
by a new component of type C
, for
example, we wouldn't need to change the interface. This is how the updated
declaration could look like:
type Sys_Base_Class_Access is access Sys_Base'Class;
type Sys_Base_Array is array (Positive range <>) of Sys_Base_Class_Access;
type AB is limited new Activation_IF and Value_Retrieval_IF with record
S_Array : Sys_Base_Array (1 .. 2);
end record;
Important
Note that we're now using the limited
keyword in the declaration of
type AB
. That is necessary because we want to prevent objects of
type AB
being copied by assignment, which would lead to two objects
having the same (dynamically allocated) subsystems A and B internally. This
change requires that both Activation_IF
and
Value_Retrieval_IF
are declared limited as well.
The body of Activate
could then allocate those components:
procedure Activate (E : in out AB) is
begin
E.S_Array := (new A, new B);
for S of E.S_Array loop
S.Activate;
end loop;
end Activate;
And the body of Deactivate
could deallocate them:
procedure Deactivate (E : in out AB) is
procedure Free is
new Ada.Unchecked_Deallocation (Sys_Base'Class, Sys_Base_Class_Access);
begin
for S of E.S_Array loop
S.Deactivate;
Free (S);
end loop;
end Deactivate;
Limited controlled types
Another approach that we could use to implement the dynamic allocation of
systems A and B is to declare AB
as a limited controlled type —
based on the Limited_Controlled
type of the Ada.Finalization
package.
The Limited_Controlled
type includes the following operations:
Initialize
, which is called when objects of a type derived from theLimited_Controlled
type are being created — by declaring an object of the derived type, for example —, andFinalize
, which is called when objects are being destroyed — for example, when an object gets out of scope at the end of a subprogram where it was created.
In this case, we must override those procedures, so we can use them for dynamic memory allocation. This is a simplified view of the update implementation:
package Simple.System_AB is
type AB is limited new Ada.Finalization.Limited_Controlled and
Activation_IF and Value_Retrieval_IF with private;
overriding procedure Initialize (E : in out AB);
overriding procedure Finalize (E : in out AB);
end Simple.System_AB;
package body Simple.System_AB is
overriding procedure Initialize (E : in out AB) is
begin
E.S_Array := (new A, new B);
end Initialize;
overriding procedure Finalize (E : in out AB) is
procedure Free is
new Ada.Unchecked_Deallocation (Sys_Base'Class, Sys_Base_Class_Access);
begin
for S of E.S_Array loop
Free (S);
end loop;
end Finalize;
end Simple.System_AB;
Updated source-code
Finally, this is the complete updated source-code example:
[Ada]
package Simple is type Activation_IF is limited interface; procedure Activate (E : in out Activation_IF) is abstract; function Is_Active (E : Activation_IF) return Boolean is abstract; procedure Deactivate (E : in out Activation_IF) is abstract; type Value_Retrieval_IF is limited interface; function Value (E : Value_Retrieval_IF) return Float is abstract; type Sys_Base is abstract new Activation_IF and Value_Retrieval_IF with private; overriding procedure Activate (E : in out Sys_Base); overriding function Is_Active (E : Sys_Base) return Boolean; overriding procedure Deactivate (E : in out Sys_Base); not overriding procedure Activation_Reset (E : in out Sys_Base) is abstract; private type Sys_Base is abstract new Activation_IF and Value_Retrieval_IF with record Active : Boolean; end record; end Simple;package body Simple is procedure Activate (E : in out Sys_Base) is begin -- NOTE: calling "E.Activation_Reset" does NOT dispatch! -- We need to use the 'Class attribute: Sys_Base'Class (E).Activation_Reset; E.Active := True; end Activate; function Is_Active (E : Sys_Base) return Boolean is (E.Active); procedure Deactivate (E : in out Sys_Base) is begin E.Active := False; end Deactivate; end Simple;package Simple.System_A is type A is new Sys_Base with private; overriding function Value (E : A) return Float; private type Val_Array is array (Positive range <>) of Float; type A is new Sys_Base with record Val : Val_Array (1 .. 2); end record; overriding procedure Activation_Reset (E : in out A); end Simple.System_A;package body Simple.System_A is procedure Activation_Reset (E : in out A) is begin E.Val := (others => 0.0); end Activation_Reset; function Value (E : A) return Float is pragma Assert (E.Val'Length = 2); begin return (E.Val (1) + E.Val (2)) / 2.0; end Value; end Simple.System_A;package Simple.System_B is type B is new Sys_Base with private; overriding function Value (E : B) return Float; private type B is new Sys_Base with record Val : Float; end record; overriding procedure Activation_Reset (E : in out B); end Simple.System_B;package body Simple.System_B is procedure Activation_Reset (E : in out B) is begin E.Val := 0.0; end Activation_Reset; function Value (E : B) return Float is (E.Val); end Simple.System_B;with Ada.Finalization; package Simple.System_AB is type AB is limited new Ada.Finalization.Limited_Controlled and Activation_IF and Value_Retrieval_IF with private; overriding procedure Activate (E : in out AB); overriding function Is_Active (E : AB) return Boolean; overriding procedure Deactivate (E : in out AB); overriding function Value (E : AB) return Float; not overriding function Check (E : AB) return Boolean; private type Sys_Base_Class_Access is access Sys_Base'Class; type Sys_Base_Array is array (Positive range <>) of Sys_Base_Class_Access; type AB is limited new Ada.Finalization.Limited_Controlled and Activation_IF and Value_Retrieval_IF with record S_Array : Sys_Base_Array (1 .. 2); end record; overriding procedure Initialize (E : in out AB); overriding procedure Finalize (E : in out AB); end Simple.System_AB;with Ada.Unchecked_Deallocation; with Simple.System_A; use Simple.System_A; with Simple.System_B; use Simple.System_B; package body Simple.System_AB is overriding procedure Initialize (E : in out AB) is begin E.S_Array := (new A, new B); end Initialize; overriding procedure Finalize (E : in out AB) is procedure Free is new Ada.Unchecked_Deallocation (Sys_Base'Class, Sys_Base_Class_Access); begin for S of E.S_Array loop Free (S); end loop; end Finalize; procedure Activate (E : in out AB) is begin for S of E.S_Array loop S.Activate; end loop; end Activate; function Is_Active (E : AB) return Boolean is (for all S of E.S_Array => S.Is_Active); procedure Deactivate (E : in out AB) is begin for S of E.S_Array loop S.Deactivate; end loop; end Deactivate; function Value (E : AB) return Float is ((E.S_Array (1).Value + E.S_Array (2).Value) / 2.0); function Check (E : AB) return Boolean is Threshold : constant := 0.1; begin return abs (E.S_Array (1).Value - E.S_Array (2).Value) < Threshold; end Check; end Simple.System_AB;with Ada.Text_IO; use Ada.Text_IO; with Simple.System_AB; use Simple.System_AB; procedure Main is procedure Display_Active (E : AB) is begin if Is_Active (E) then Put_Line ("System AB is active"); else Put_Line ("System AB is not active"); end if; end Display_Active; procedure Display_Check (E : AB) is begin if Check (E) then Put_Line ("System AB check: PASSED"); else Put_Line ("System AB check: FAILED"); end if; end Display_Check; S : AB; begin Put_Line ("Activating system AB..."); Activate (S); Display_Active (S); Display_Check (S); Put_Line ("Deactivating system AB..."); Deactivate (S); Display_Active (S); end Main;
Naturally, this is by no means the best possible implementation of system AB. By applying other software design strategies that we haven't covered here, we could most probably think of different ways to use object-oriented programming to improve this implementation. Also, in comparison to the original implementation, we recognize that the amount of source-code has grown. On the other hand, we now have a system that is factored nicely, and also more extensible.