February 3, 1998
by Jean-Marie Dautelle
To produce "good" Ada 95 code using object-oriented methodology is certainly not obvious. Despite the comprehensive documentation available, there are many pitfalls along the way. This article is the fruit of experience gained through wanderings, trials and failures.
There is one problem with people who know everything: They don't learn anything ! Anonymous
Almost everybody (but not everybody) agrees that a class in Ada 95 corresponds to a package containing a main type. In most of the cases the main type is declared private or with a private extension. Other types may also be declared within a package specification, we will refer to them as regular types (not class), they are used for interfacing purposes (when public) or to define internal data structures (when private).
with Angle; package Coordinates is type Object is ... -- Class type (either private or derived with private extension). type Geographic is record -- Regular type (public) Latitude : Angle.Radian; Longitude : Angle.Radian; end record; end Coordinates;
That is the basics, but still, the programmer has many choices: should one use tagged types? controlled types? ... Let's discuss the details further.
This rule is simple: Do not tag very simple classes such as Distance, Angle, Temperature ...
with Scalar; package Angle is type Object is private; Nil : constant Object; type Radian is new Scalar.Float_15; type Degree is new Scalar.Float_15; function Radian_Of (The_Angle : in Angle.Object) return Angle.Radian; function Set_Radian (To : in Angle.Radian; Affecting : in out Angle.Object); function Degree_Of (The_Angle : in Angle.Object) return Angle.Degree; function Set_Degree (To : in Angle.Degree; Affecting : in out Angle.Object); function "+" (Left, Right : in Angle.Object) return Angle.Object; ... private type Object is record In_Radian : Angle.Radian := Angle.Radian'Last; end record; Nil : constant Angle.Object := (In_Radian => Angle.Radian'Last); end Angle;
When a class is derived from Ada.Finalization.Controlled, the call to Initialize, Adjust and Finalize is automatically done by the compiler (See Reference Manual). This is useful in order to avoid memory leaks (the compiler will never forget to delete an object which is no longer used). It is also useful when an object is duplicated: The two copies can be made independent. This feature should be used only if the default behavior of the compiler is not satisfactory. Indeed, once a class is controlled, the programmer is assumed to take over the controlled processing for each of its children. Thus, if a child adds new extensions, the inherited Initialize, Adjust and Finalize will not consider the new extensions and therefore will have to be overloaded (see: Beware of inheritance).
In order to avoid unpredictable behavior when objects not initialized are used, the class type declaration should set default values for each class attribute. Consequently, each class should define a public Nil object, which can be used as a default attribute value within the context of higher level classes declaration. For simple type such as Integer_32 or Float_6, default value corresponding to limit cases can be used instead (Integer_32'Last, Float_6'Last). For enumerated types a default value corresponding to the default case is used.
If a class declares a function 'Create' returning an instance of a class, all the child classes inherit that operation. This can be hazardous, especially if the child class has more attributes than its parent (note: to make programmers pay attention, the inherited function will be abstract unless overridden). Indeed, the inherited 'Create' might not initialize these attributes. Two solutions are proposed in the Ada 95 Guideline in order to prevent the Create from being inherited:
Neither solution is really satisfactory:
The first solution obliges to explicitly de-allocate each instance when it is no longer used. In some cases the de-allocation is not even possible, ie. A := Create (..).all;
The second solution makes automatic code generation difficult. Also, it forces objects to be copied at least once after they are created, which is a performance hit.
Hopefully, in Ada an explicit 'Create' routine is not really necessary. Any instance of a class is automatically created either at its declaration (static) or using dynamic allocation, ie. Instance := new Class.Object. Therefore, only initialization routines have to be defined. If the default initialization is not satisfactory, new initialization routines may be defined (with or without extra parameters). In order for these routines not to be inherited, they will take as main parameter an object of class-wide type ('Class). For simple classes, the 'Set_<<Attribute_Name>>' method may be used in place of 'Initialize'.
with Ada.Finalization; with Color, Engine, Speed; package Car is type Object is new Ada.Finalization.Controlled with private; type Reference is access all Object'Class; type View is access constant Object'Class; Nil : constant Object; type Size is (Small, Normal, Big, Unknown); subtype Valid_Size is Size range Small .. Big; procedure Initialize (The_Car : in out Car.Object); -- Optional procedure Adjust (The_Car : in out Car.Object); -- Optional procedure Finalize (The_Car : in out Car.Object); -- Optional procedure Initialize (The_Car : in out Car.Object'Class; -- Class-wide With_Color : in Color.Object; With_Size : in Car.Valid_Size; With_Engine : in Engine.Reference); procedure Set_Color (To : in Color.Object; Affecting : in out Car.Object); procedure Set_Size (To : in Car.Valid_Size; Affecting : in out Car.Object); function Color_Of (The_Car : in Car.Object) return Color.Object; function Speed_Of (The_Car : in Car.Object) return Speed.Object; private type Object is new Ada.Finalization.Controlled with record Size : Car.Size := Unknown; Own_Color : Color.Object := Color.Nil; Own_Engine : Engine.Reference := null; ... end record; Nil : constant Car.Object := (Ada.Finalization.Controlled with Size => Unknown, Own_Color => Color.Nil, Own_Engine => null, ...); end Car; package body Car is procedure Initialize (The_Car : in out Car.Object'Class; With_Color : in Color.Object; With_Size : in Car.Valid_Size; With_Engine : in Engine.Reference) is begin -- Dispatching call, if car is derived, the child's 'Set_Color' is called. -- Set_Color (To => With_Color, Affecting => The_Car); -- Non-dispatching call, 'Car.Set_Size' is always called. -- Set_Size (To => With_Size, Affecting => Car.Object(The_Car)); The_Car.Engine := With_Engine; end Initialize; ... end Car;
If the classes are controlled, the 'Delete' routine can be advantageously replaced with 'Finalize'. If the class is not controlled, there is no use for 'Delete'. Simple, isn't it?
This section intends to be complementary to the Ada 95 Quality and Style Guide (Chapter 3.2). It provides naming rules proper to object-oriented development using Ada 95.
We offer three rules for coherent naming of the entities defined when creating a class:
package Angle is type Object is private; type Reference is access all Object'Class; type View is access constant Object'Class; Nil : constant Object; ... end Angle; generic Item ... package List is type Object is new Ada.Finalization.Controlled with private; type Reference is access all Object'Class; type View is access constant Object'Class; Nil : constant Object; ... end List;
This naming scheme remains consistent even when parameterized classes are instantiated. Let us look at the following bad naming convention widely spread in the community:
generic ... package List_Pkg is type List is new ... end List_Pkg; package Fruit_List_Pkg is new List_Pkg (...)The name of the Fruit_List_Pkg main type is not Fruit_List_Pkg.Fruit_List as one would reasonably expect (in a non-generic solution) but a plain 'Fruit_List_Pkg.List'.
We offer four rules:
Insert (The_Item => An_Apple, Before_Index => 3, Into => The_Fruit_List); Merge (Left => My_Fruit_List, Right => The_Citrus_Fruits); Is_Empty (The_Queue); -- Positional association used.
If rule 4 is not respected the readability of inherited operations can be affected. Let us see the following case where rule 4 is transgressed:
package List is ... procedure Pop (The_Item : out List.Item; From_List : in out List.Object); -- Use of From_List instead -- of From. end List; package Queue is ... type Object is new List.Object; -- Queue.Object inherits -- List's operations. end; with List, Queue; procedure Main is begin ... Queue.Pop( The_Item => My_Item From_List => The_Queue); -- Not very clear, the name specifies -- a list but the argument is a Queue ?! end Main;
Often, objects belonging to the same class have unique identifier (or key) attributes. If these identifiers are specified at the initialization of the object, how can we ensure that identifier duplication is avoided ?
The class itself keeps track of the objects initialized (internal collection). The initialization of an object is done through an Initialize routine (see: The myth of object creation.), therefore the Initialize can check if the identifier is already in use by looking to an internal table containing the identifiers used. The problem with this solution is that it does its job too well. Indeed, it prevents transitory duplication of the objects and independent duplication for backup or simulation purposes.
The unity requirement is allocated to an external collection. Instead of allocating the unity requirement to the class itself, it is always possible to allocate this requirement to a collection containing the instances of the class. The check for object duplication is done when the object is inserted into the collection. In a lot of cases this external collection makes sense, therefore, there is no extra work associated with the duplication check.
Each instance of the class Car contains a plate number that is supposed to be unique. A possible implementation might be:
package Car is type Plate_Number is ... -- The Initialize routine doesn't check if the plate number -- is duplicated. -- procedure Initialize( The_Car : in out Car.Object'Class With_Plate : Car.Plate_Number); ... ); ... end Car; -- A collection is defined. -- package Car_Table is new Table( Item => Car.Reference; Id => Car.Plate_Number; ...); procedure Main is ... begin ... Car.Initialize (The_Car => My_Car; With_Plate => My_Plate; ...); Car_Table.Add( The_Item => My_Car, Into => The_Car_Table, Using_Policy => Raise_Exception); -- An exception is raised if -- the plate number is -- duplicated. ... end Main;
For predefined types such as Integer and Float the language reference manual guarantees the minimum range or precision, but any implementation can exceed these values. Therefore two different compilers can have distinct range and/or precision for the predefined types Integer and Float. In some cases portability can be affected, ie. my code executes fine with the compiler A which uses 32 bits for Integer, but would fail with the compiler B which uses only 16 bits (minimum required).
The solution to the portability issue for predefined scalar types is simple : Do not use them ! Instead define your own types whose ranges and precision are forced.
Here is a possible definition of your own scalar types:
package Scalar is type Integer_8 is range -2**7 .. 2**7-1; subtype Natural_8 is Integer_8 range 0 .. 2**7-1; subtype Positive_8 is Integer_8 range 1 .. 2**7-1; for Integer_8'Size use 8; type Integer_16 is range -2**15 .. 2**15-1; subtype Natural_16 is Integer_16 range 0 .. 2**15-1; subtype Positive_16 is Integer_16 range 1 .. 2**15-1; for Integer_16'Size use 16; type Integer_32 is range -2**31 .. 2**31-1; subtype Natural_32 is Integer_32 range 0 .. 2**31-1; subtype Positive_32 is Integer_32 range 1 .. 2**31-1; for Integer_32'Size use 32; type Float_6 is digits 6; for Float_6'Size use 32; type Float_15 is digits 15; for Float_15'Size use 64; end Scalar;
In Ada, a parameterized class maps directly to generic package. Some notation allows parameterized class to add new methods (Booch), with others (UML) parameterized classes have to be derived first in order to add (or overload) class methods. Ada generics correspond to that last case.
If you have to modify or to add any specific behavior to your parameterized class, an intermediate package is used for derivation purpose. It's a good idea to adopt a consistent naming convention for these packages (e.g. <<Class_Name>>_Instance). Also, all the exceptions and regular types of the intermediate package are renamed in order to make the extension transparent to the client packages.
At first, methods defined within 'List' are satisfactory for Employee_List:
with List, Employee; package Employee_List is new List (Item => Employee.Object);
Later (development is an incremental process) the method 'Search' is added to the class Employee_List.
with List, Employee; package Employee_List_Instance is new List (Item => Employee.Object); with Employee_List_Instance, Personne, Employee; package Employee_List is type Object is new Employee_List_Instance.Object with private; type Reference is access all Object'Class; type View is access constant Object'Class; Nil : constant Object; ... -- Rename all regular types and exceptions of Employee_List_Instance. -- First_Of_Empty_List_Error : exception renames Employee_List_Instance.First_Of_Empty_List_Error; ... subtype Length is Employee_List_Instance.Length; ... -- Add the new method. -- Search_Not_Found_Error : exception; procedure Search (The_Employee : out Employee.View; With_Name : in Personne.Name; Within : in Employee_List.Object); private ... end Employee_List;
When a class is derived, all the methods primitive to its ancestors are inherited. But, sometimes, some of the inherited methods will not work for the derived class, (ie. They don't take into account new attributes), it is necessary to overload them.
Forgetting to overload a method that should have been, is one of the most common errors, it cannot be found for sure during unit testing, only a thorough inspection of all the inherited methods in the context of the derived class may detect this oversight.
In Ada 95, controlled objects are first citizen candidates for such neglect, any controlled attribute added to a derived class will force to overload the inherited 'Adjust' and 'Finalize' routines (at the minimum, they will have to call the adjustment and finalization for the new attribute).
Here, the class Position is derived from Coordinates, but the method 'Distance_Between' has to be overloaded to take into account the new altitude component of the position vector.
package Coordinates is type Object is tagged private; ... function Distance_Between (Left : in Coordinates.Object; Right: in Coordinates.Object) return Distance.Object; ... end Coordinates; package Position is type Object is new Coordinates.Object with private; ... function Distance_Between (Left : in Position.Object; -- Overload of Right : in Position.Object) -- Coordinates.Distance_Between return Distance.Object; ... end Position;
The implementation of the Finalize and Adjust operations for controlled objects is very delicate and there are numerous pitfalls along the way. Also, debugging is delicate because any exception raised within these procedures during an assignment generates a cryptic 'Program_Error'. Here is a list of hints which should help in implementing bug-free Adjust and Finalize routines:
The ability to pass subprograms as parameters is one of the nicest features of Ada 95 (it didn't exist in Ada 83). But accessibility rules make it useless within a multi-tasking program. Indeed, access to a local procedure cannot be used as a parameter of a more globally declared procedure (e.g. a class method). In practice, accessibility rules force the use of global variable which makes multi-tasking very delicate. The only alternative is to use generic parameters (parameterized methods), with the inconvenience that parameterized methods are not inheritable.
The following looks nice, but it will not compile!
generic type Item is private; package List is type Object is ... ... type Action is access procedure Action( The_Item : in out Item; Stop_Iterating : out Boolean); procedure Iterate ( The_Action : Action Through : in List.Object); ... end List; with List, Student; package Student_List is new List (Item => Student.Object); with Student_List; function Retrieve_Student (With_Name : Student.Name; From : Student_List.Object) return Student.Object is The_Student_Found : Student.Object; procedure Find (The_Student : in out Student.Object; Stop_Iterating : out Boolean) is begin if Student.Name_Of (The_Student) = With_Name then The_Student_Found := The_Student; Stop_Iterating := True; else Stop_Iterating := False; end if; end Find; begin Student_List.Iterate (The_Action => Find'Access, -- Compilation Error Through => From); return The_Student_Found; end Retrieve_Student;
It is interesting to note that Ada 95 does not provide the 'Unchecked_Access attribute for subprograms (unlike what is available for variables). That would have solved our problem.
Here is an alternative solution using genericity.
generic type Item is private; package List is ... generic with procedure Action( The_Item : in out Item; Stop_Iterating : out Boolean); procedure Iterate (Through : in List.Object'Class); -- Wide class necessary, -- parameterized methods are -- not inheritable. ... end List;
Ada 95 doesn't support multiple parents inheritance (at least not simply). It is not a real issue; one may have a problem imagining for example an object which is at the same time let's say an elephant and the picture of an elephant! However, interface inheritance (such as found in the Java language) would be greatly appreciated. The notion of abstract class is already embedded in the language. Why shouldn't a class be allowed to implement several abstract classes?
|Do you have a noteworthy Ada 95 lesson? Let's develop a programming and design tradition around Ada 95!|
Page last modified: 1998-02-03