Ada Style Guide/Program Structure

Readability · Programming Practices

Introduction

edit

Proper structure improves program clarity. This is analogous to readability on lower levels and facilitates the use of the readability guidelines (Chapter 3). The various program structuring facilities provided by Ada were designed to enhance overall clarity of design. These guidelines show how to use these facilities for their intended purposes.

The concept of child packages supports the concept of subsystem, where a subsystem is represented in Ada as a hierarchy of library units. In general, a large system should be structured as a series of subsystems. Subsystems should be used to represent logically related library units, which together implement a single, high-level abstraction or framework.

Abstraction and encapsulation are supported by the package concept and by private types. Related data and subprograms can be grouped together and seen by a higher level as a single entity. Information hiding is enforced via strong typing and by the separation of package and subprogram specifications from their bodies. Exceptions and tasks are additional Ada language elements that impact program structure.

High-Level Structure

edit

Well-structured programs are easily understood, enhanced, and maintained. Poorly structured programs are frequently restructured during maintenance just to make the job easier. Many of the guidelines listed below are often given as general program design guidelines.

Separate Compilation Capabilities

edit

guideline

edit
  • Place the specification of each library unit package in a separate file from its body.
  • Avoid defining library unit subprograms that are not intended to be used as main programs. If such subprograms are defined, then create an explicit specification, in a separate file, for each library unit subprogram.
  • Minimize the use of subunits.
  • In preference to subunits, use child library units to structure a subsystem into manageable units.
  • Place each subunit in a separate file.
  • Use a consistent file naming convention.
  • In preference to nesting in a package body, use a private child and with it to the parent body.
  • Use private child unit specifications for data and subprograms that are required by (other) child units that extend a parent unit's abstraction or services.

example

edit

The file names below illustrate one possible file organization and associated consistent naming convention. The library unit name uses the adb suffix for the body. The suffix ads indicates the specification, and any files containing subunits use names constructed by separating the body name from the subunit name with an underscore:

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

Depending on what characters your file system allows you to use in file names, you could show the distinction between parent and subunit name more clearly in the file name. If your file system allows the "#" character, for example, you could separate the body name from the subunit name with a #:

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

Some operating systems are case-sensitive, although Ada itself is not a case-sensitive language. For example, you could choose a convention of all lowercase file names.

rationale

edit

The main reason for the emphasis on separate files in this guideline is to minimize the amount of recompilation required after each change. Typically, during software development, bodies of units are updated far more often than specifications. If the body and specification reside in the same file, then the specification will be compiled each time the body is compiled, even though the specification has not changed. Because the specification defines the interface between the unit and all of its users, this recompilation of the specification typically makes recompilation of all users necessary in order to verify compliance with the specification. If the specifications and bodies of the users also reside together, then any users of these units will also have to be recompiled and so on. The ripple effect can force a huge number of compilations that could have been avoided, severely slowing the development and test phase of a project. This is why you should place specifications of all library units (nonnested units) in separate files from their bodies.

Library unit subprograms should be minimized. The only real use for library unit subprograms is as the main subprogram. In almost all other cases, it is better to embed the subprogram into a package. This provides a place (the package body) to localize data needed by the subprogram. Moreover, it cuts down on the number of separate modules in the system.

In general, you should use a separate specification for any library subprogram that is mentioned in a with clause. This makes the with'ing unit dependent on the library subprogram specification, not its body.

You should minimize the use of subunits because they create maintenance problems. Declarations appearing in the parent body are visible in the subunit, increasing the amount of data global to the subunit and, thus, increasing the potential ripple effect of changes. Subunits hinder reuse because they provide an incentive to put otherwise reusable code in the subunit directly rather than in a common routine called from multiple subprograms.

With the availability of child library units in Ada 95, you can avoid most uses of subunits. For example, instead of using a subunit for a large nested body, you should try to encapsulate this code in a child library unit and add the necessary context clauses. You can modify the body of the child unit without having to recompile any of the other units in a subsystem.

An additional benefit of using multiple, separate files is that it allows different implementors to modify different parts of the system at the same time with conventional editors, which do not allow multiple concurrent updates to a single file.

Finally, keeping bodies and specifications separate makes it possible to have multiple bodies for the same specification or multiple specifications for the same body. Although Ada requires that there be exactly one specification per body in a system at any given time, it can still be useful to maintain multiple bodies or multiple specifications for use in different builds of a system. For example, a single specification may have multiple bodies, each of which implements the same functionality with a different tradeoff of time versus space efficiency, or, for machine-dependent code, there may be one body for each target machine. Maintaining multiple package specifications can also be useful during development and test. You may develop one specification for delivery to your customer and another for unit testing. The first one would export only those subprograms intended to be called from outside of the package during normal operation of the system. The second one would export all subprograms of the package so that each of them could be independently tested.

A consistent file naming convention is recommended to make it easier to manage the large number of files that may result from following this guideline.

In implementing the abstraction defined in a package specification, you often need to write supporting subprograms that manipulate the internal representation of the data. These subprograms should not be exported on the interface. You have a choice of whether to place them in the package body of the parent program or in a child package named in a context clause of the parent package body. When you place them in the parent package body, you make them inaccessible to all clients of the parent, including extensions of the parent declared in child packages. If these subprograms are needed to implement extensions of the parent abstraction, you would be forced to modify both the parent specification and the body because you would have to declare the extensions within the parent specification. This technique would then force recompilation of the entire package (specification and body) as well as all its clients.

Alternatively, you can implement the supporting subprograms in a private child package. Because the parent unit's specification is not modified, neither it nor its clients need to be recompiled. The data and subprograms that might have declared in the parent unit body must now be declared in the private child unit's specification to make them visible to both the parent unit body and to any child units that extend the parent unit's services or abstractions. (See also Guidelines 4.1.6 and 4.2.) This use of private child units will generally minimize recompilations within the unit family and among its clients.

In declaring the child package private, you achieve a similar effect to declaring it in the parent package body to the extent that clients of the parent cannot name the private child in a context clause. You gain flexibility because now you can extend the parent abstraction using child packages without having to recompile the parent specification or its body, assuming that you do not otherwise modify the parent or its body. This added flexibility will usually compensate for the increased dependency between units, in this case, the additional context clause on the parent body (and other child package bodies) that names the private child package of supporting subprograms.

Configuration Pragmas

edit

guideline

edit
  • When possible, express configuration pragmas through compiler options or other means that do not require modifications to the source code.
  • When configuration pragmas must be placed in source code, consider isolating them to one compilation unit per partition; if specified, the main subprogram for the partition is recommended.

rationale

edit

Configuration pragmas are generally used to select a partition-wide or system-wide option. Usually, they reflect either high-level software architecture decisions (e.g., pragma Task_Dispatching_Policy) or the use of the software in a particular application domain (e.g., safety-critical software). If a configuration pragma is embedded within a software component and that component is reused in a different context where the pragma is no longer appropriate, then it may cause problems in the new application. Such problems can include the rejection by the compilation system of otherwise legal source code or unexpected behavior at run-time. These problems can be significant given the wide scope of a configuration pragma. In addition, maintenance of the original system may require that some of these system-wide decisions be changed. If the configuration pragmas are scattered throughout the software, it may be difficult to locate the lines that need to change.

As a result, it is recommended that all configuration pragmas be kept in a single compilation unit if possible to make them easy to locate and modify as needed. If this compilation unit is unlikely to be reused (e.g., a main subprogram), then the likelihood of conflicts with future reusers is reduced. Finally, if these system-wide decisions are indicated without embedding them in the code at all, such as through a compiler option, then the problems described above are even less likely to occur.

exceptions

edit

Certain pragmas (e.g., pragma Suppress) can be used in several forms, including as a configuration pragma. This guideline does not apply to such pragmas when they are not used as a configuration pragma.

Subprograms

edit

guideline

edit
  • Use subprograms to enhance abstraction.
  • Restrict each subprogram to the performance of a single action.

example

edit

Your program is required to draw a menu of user options as part of a menu-driven user interface package. Because the contents of the menu can vary depending on the user state, the proper way to do this is to write a subprogram to draw the menu. This way, the output subprogram has one purpose and the way to determine the menu content is described elsewhere.

...
----------------------------------------------------------------------
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;
----------------------------------------------------------------------

rationale

edit

Subprograms are an extremely effective and well-understood abstraction technique. Subprograms increase program readability by hiding the details of a particular activity. It is not necessary that a subprogram be called more than once to justify its existence.

notes

edit

Guideline 10.7.1 discusses dealing with the overhead of subroutine calls.

Functions

edit

guideline

edit
  • Use a function when the subprogram's primary purpose is to provide a single value.
  • Minimize the side effect of a function.
  • Consider using a parameterless function when the value does not need to be static.
  • Use a parameterless function (instead of a constant) if the value should be inherited by types derived from the type.
  • Use a parameterless function if the value itself is subject to change.

example

edit

Although reading a character from a file will change what character is read next, this is accepted as a minor side effect compared to the primary purpose of the following function:

function Next_Character return Character is separate;

However, the use of a function like this could lead to a subtle problem. Any time the order of evaluation is undefined, the order of the values returned by the function will effectively be undefined. In this example, the order of the characters placed in Word and the order that the following two characters are given to the Suffix parameters are unknown. No implementation of the Next_Character function can guarantee which character will go where:

   Word : constant String := String'(1 .. 5 => Next_Character);
begin  -- Start_Parsing
   Parse(Keyword => Word,
         Suffix1 => Next_Character,
         Suffix2 => Next_Character);
end Start_Parsing;

Of course, if the order is unimportant (as in a random number generator), then the order of evaluation is unimportant.

The following example shows the use of a parameterless function instead of a constant:

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

This same example could have been written using constants:

type T is private;
Nil : constant T;
Default : constant T;

rationale

edit

A side effect is a change to any variable that is not local to the subprogram. This includes changes to variables by other subprograms and entries during calls from the function if the changes persist after the function returns. Side effects are discouraged because they are difficult to understand and maintain. Additionally, the Ada language does not define the order in which functions are evaluated when they occur in expressions or as actual parameters to subprograms. Therefore, a program that depends on the order in which side effects of functions occur is erroneous. Avoid using side effects anywhere.

Packages

edit

guideline

edit
  • Use packages for information hiding.
  • Use packages with tagged types and private types for abstract data types.
  • Use packages to model abstract entities appropriate to the problem domain.
  • Use packages to group together related type and object declarations (e.g., common declarations for two or more library units).
  • Encapsulate machine dependencies in packages. Place a software interface to a particular device in a package to facilitate a change to a different device.
  • Place low-level implementation decisions or interfaces in subprograms within packages.
  • Use packages and subprograms to encapsulate and hide program details that may change (Nissen and Wallis 1984).

example

edit

Reading the names and other attributes of external files is highly machine dependent. A package called Directory could contain type and subprogram declarations to support a generalized view of an external directory that contains external files. Its internals may, in turn, depend on other packages more specific to the hardware or operating system:

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;

rationale

edit

Packages are the principal structuring facility in Ada. They are intended to be used as direct support for abstraction, information hiding, and modularization. For example, they are useful for encapsulating machine dependencies as an aid to portability. A single specification can have multiple bodies isolating implementation-specific information so other parts of the code do not need to change.

Encapsulating areas of potential change helps to minimize the effort required to implement that change by preventing unnecessary dependencies among unrelated parts of the system.

notes

edit

The most prevalent objection to this guideline usually involves performance penalties. See Guideline 10.7.1 for a discussion about subprogram overhead.

Child Library Units

edit

guideline

edit
  • If a new library unit represents a logical extension to the original abstraction, define it as a child library unit.
  • If a new library unit is independent (e.g., introduces a new abstraction that depends only in part on the existing one), then encapsulate the new abstraction in a separate library unit.
  • Use child packages to implement a subsystem.
  • Use public child units for those parts of a subsystem that should be visible to clients of the subsystem.
  • Use private child units for those parts of a subsystem that should not be visible to clients of the subsystem.
  • Use private child units for local declarations used only in implementing the package specification.
  • Use child packages to implement constructors, even when they return access values.

example

edit

The following example of a windowing system is taken from Cohen et al. (1993) and illustrates some of the uses of child units in designing subsystems. The parent (root) package declares the types, subtypes, and constants that its clients and subsystems need. Individual child packages provide specific parts of the windowing abstraction, such as atoms, fonts, graphic output, cursors, and keyboard information:

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;

rationale

edit

The user can create more precise packages with less cluttered interfaces, using child library packages to extend the interfaces as needed. The parent contains only the relevant functionality. The parent provides a general-purpose interface, while the child units provide more complete programming interfaces, tailored to that aspect of an abstraction that they are extending or defining.

Child packages build on the modular strength of Ada where "the distinct specification and body decouple the user interface to a package (the specification) from its implementation (the body)" (Rationale 1995, §II.7). Child packages provide the added capability of being able to extend a parent package without recompiling the parent or the parent's clients.

Child packages allow you to write logically distinct packages that share a private type. The visibility rules give the private part of the child specification and the body of the child visibility into the private part of the parent. Thus, you can avoid creating a monolithic package for the sake of developing abstractions that share a private type and need to know its representation. The private representation is not available to clients of the package, so the abstraction in the package and its children is maintained.

Using private child packages for local declarations enables you to have available the support declarations you need when implementing both the parent package and extensions to the parent package. You enhance the maintainability of your program by using a common set of support declarations (data representations, data manipulation subprograms). You can modify the internal representation and the implementation of the support subprograms without modifying or recompiling the rest of your subsystem because these support subprograms are implemented in the body of the private child package. See also Guidelines 4.1.1, 4.2.1, 8.4.1, and 8.4.8.

See also Guideline 9.4.1 for a discussion of the use of child library units in creating a tagged type hierarchy.

Cohesion

edit

guideline

edit
  • Make each package serve a single purpose.
  • Use packages to group related data, types, and subprograms.
  • Avoid collections of unrelated objects and subprograms (NASA 1987; Nissen and Wallis 1984).
  • Consider restructuring a system to move two highly related units into the same package (or package hierarchy) or to move relatively independent units into separate packages.

example

edit

As a bad example, a package named Project_Definitions is obviously a "catch all" for a particular project and is likely to be a jumbled mess. It probably has this form to permit project members to incorporate a single with clause into their software.

Better examples are packages called Display_Format_Definitions, containing all the types and constants needed by some specific display in a specific format, and Cartridge_Tape_Handler, containing all the types, constants, and subprograms that provide an interface to a special-purpose device.

rationale

edit

The degree to which the entities in a package are related has a direct impact on the ease of understanding packages and programs made up of packages. There are different criteria for grouping, and some criteria are less effective than others. Grouping the class of data or activity (e.g., initialization modules) or grouping data or activities based on their timing characteristics is less effective than grouping based on function or need to communicate through data (Charette 1986).

The "correct" structuring of a system can make a tremendous difference in the maintainability of a system. Although it may seem painful at the time, it is important to restructure if the initial structuring is not quite right.

See also Guideline 5.4.2 on heterogeneous data.

notes

edit

Traditional subroutine libraries often group functionally unrelated subroutines. Even such libraries should be broken into a collection of packages, each containing a logically cohesive set of subprograms.

Data Coupling

edit

guideline

edit
  • Avoid declaring variables in package specifications.

example

edit

This is part of a compiler. Both the package handling error messages and the package containing the code generator need to know the current line number. Rather than storing this in a shared variable of type Natural, the information is stored in a package that hides the details of how such information is represented and makes it available with access routines:

-------------------------------------------------------------------------
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;
-------------------------------------------------------------------------

rationale

edit

Strongly coupled program units can be difficult to debug and very difficult to maintain. By protecting shared data with access functions, the coupling is lessened. This prevents dependence on the data structure, and access to the data can be controlled.

notes

edit

The most prevalent objection to this guideline usually involves performance penalties. When a variable is moved to the package body, subprograms to access the variable must be provided and the overhead involved during each call to those subprograms is introduced. See Guideline 10.7.1 for a discussion about subprogram overhead.

Tasks

edit

guideline

edit
  • Use tasks to model abstract, asynchronous entities within the problem domain.
  • Use tasks to define concurrent algorithms for multiprocessor architectures.
  • Use tasks to perform concurrent, cyclic, or prioritized activities (NASA 1987).

rationale

edit

The rationale for this guideline is given under Guideline 6.1.2. Chapter 6 discusses tasking in more detail.

Protected Types

edit

guideline

edit
  • Use protected types to control or synchronize access to data or devices.
  • Use protected types to implement synchronization tasks, such as a passive resource monitor.

example

edit

See example in Guideline 6.1.1.

rationale

edit

The rationale for this guideline is given under Guideline 6.1.1. Chapter 6 discusses concurrency and protected types in more detail.

Visibility

edit

Ada's ability to enforce information hiding and separation of concerns through its visibility controlling features is one of the most important advantages of the language, particularly when "pieces of a large system are being developed separately." Subverting these features, for example, by excessive reliance on the use clause, is wasteful and dangerous. See also Guidelines 5.7 and 9.4.1.

Minimization of Interfaces

edit

guideline

edit
  • Put only what is needed for the use of a package into its specification.
  • Minimize the number of declarations in package specifications.
  • Do not include extra operations simply because they are easy to build.
  • Minimize the context (with) clauses in a package specification.
  • Reconsider subprograms that seem to require large numbers of parameters.
  • Do not manipulate global data within a subprogram or package merely to limit the number of parameters.
  • Avoid unnecessary visibility; hide the implementation details of a program unit from its users.
  • Use child library units to control the visibility of parts of a subsystem interface.
  • Use private child packages for those declarations that should not be used outside the subsystem.
  • Use child library units present different views of an entity to different clients.
  • Design (and redesign) interfaces after having worked out the logic of various expected clients of the interface.

example

edit
-------------------------------------------------------------------------
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;
-------------------------------------------------------------------------

rationale

edit

For each entity in the specification, give careful consideration to whether it could be moved to a child package or to the parent package body. The fewer the extraneous details, the more understandable the program, package, or subprogram. It is important to maintainers to know exactly what a package interface is so that they can understand the effects of changes. Interfaces to a subprogram extend beyond the parameters. Any modification of global data from within a package or subprogram is an undocumented interface to the "outside" as well.

Minimize the context clauses on a specification by moving unnecessary clauses to the body. This technique makes the reader's job easier, localizes the recompilation required when library units change, and helps prevent a ripple effect during modifications. See also Guideline 4.2.3.

Subprograms with large numbers of parameters often indicate poor design decisions (e.g., the functional boundaries of the subprogram are inappropriate or parameters are structured poorly). Conversely, subprograms with no parameters are likely to be accessing global data.

Objects visible within package specifications can be modified by any unit that has visibility to them. The object cannot be protected or represented abstractly by its enclosing package. Objects that must persist should be declared in package bodies. Objects whose value depends on program units external to their enclosing package are probably either in the wrong package or are better accessed by a subprogram specified in the package specification.

Child library units can provide distinct views of the hierarchical library. The engineer can provide a different view for the client than for the implementor (Rationale 1995, §10.1). By creating private child packages, the engineer can provide facilities that are only available inside the subsystem rooted at the parent library unit. The declarations inside a private child package specification are not exported outside the subsystem. Thus, the engineer can declare utilities needed to implement an abstraction in a private child package (e.g., debugging utilities [Cohen et al. 1993]) and be certain that users of the abstraction (i.e., the clients) cannot access these utilities.

Different clients may have different needs for essentially the same resource. Instead of having multiple versions of the resources, consider having child units that export different views for different purposes.

Designing an interface based strictly on predicting what clients "might" need can produce a bloated and inappropriate interface. What then happens is that clients try to "live" with the interface and work around the inappropriate interfaces, repeating code that logically should be part of the shared abstraction. See Guideline 8.3.1 for a discussion of interfaces from the reusability perspective.

notes

edit

In some cases, subroutine libraries look like large, monolithic packages. In such cases, it may be beneficial to break these up into smaller packages, grouping them according to category (e.g., trigonometric functions).

Nested Packages

edit

guideline

edit
  • Use child packages rather than nested packages to present different views of the same abstraction.
  • Nest package specifications within another package specification only for grouping operations or hiding common implementation details.

example

edit

Annex A of the Ada Reference Manual (1995) gives an example of package specification nesting. The specification of the generic package Generic_Bounded_Length is nested inside the specification of package Ada.Strings.Bounded. The nested package is a generic, grouping closely related operations.

rationale

edit

Grouping package specifications into an encompassing package emphasizes a relationship of commonality among those packages. It also allows them to share common implementation details resulting from the relationship. Nesting packages allows you to organize the name space of the package in contrast to the semantic effect of nesting inside of subprograms or task bodies.

An abstraction occasionally needs to present different views to different classes of users. Building one view upon another as an additional abstraction does not always suffice because the functionality of the operations presented by the views may be only partially disjointed. Nesting specifications groups the facilities of the various views, yet associates them with the abstraction they present. Abusive mixing of the views by another unit would be easy to detect due to the multiple use clauses or an incongruous mix of qualified names.

See the rationale discussed in Guideline 4.2.1.

Restricting Visibility

edit

guideline

edit
  • Consider using private child packages in lieu of nesting.
  • Restrict the visibility of program units as much as possible by nesting them inside package bodies (Nissen and Wallis 1984) if you cannot use a private child package.
  • Minimize nesting program units inside subprograms and tasks.
  • Minimize the scope within which with clauses apply.
  • Only with those units directly needed.

example

edit

This program illustrates the use of child library units to restrict visibility. The procedure Rational_Numbers.Reduce is nested inside the body of Rational_Numbers to restrict its visibility to the implementation of this abstraction. Rather than make the text input/output facilities visible to the entire rational number hierarchy, it is only available to the body of the child library Rational_Numbers.IO. This example is adapted from the Ada Reference Manual (1995, §§7.1 [Annotated], 7.2 [Annotated], and 10.1.1 [Annotated]):

-------------------------------------------------------------------------
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;

rationale

edit

Restricting visibility of a program unit ensures that the program unit is not called from some part of the system other than that which was intended. This is done by nesting it inside the only unit that uses it, by hiding it inside a package body rather than declaring it in the package specification, or by declaring it as a private child unit. This avoids errors and eases the job of maintainers by guaranteeing that a local change in that unit will not have an unforeseen global effect.

Restricting visibility of a library unit by using with clauses on subunits rather than on the entire parent unit is useful in the same way. In the example above, it is clear that the package Text_IO is used only by the Listing_Facilities package of the compiler.

Nesting inside subprograms and tasks is discouraged because it leads to unreusable components. These components are essentially unreusable because they make undesirable up-level references into the defining context. Unless you truly want to ensure that the program unit is not called from some unintended part of the system, you should minimize this form of nesting.

See also Guideline 4.2.1 for a discussion of the use of child units.

notes

edit

One way to minimize the coverage of a with clause is to use it only with subunits that really need it. Consider making those subunits separate compilation units when the need for visibility to a library unit is restricted to a subprogram or two.

Hiding Tasks

edit

guideline

edit
  • Carefully consider encapsulation of tasks.

example

edit
-------------------------------------------------------------------------
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;
-------------------------------------------------------------------------

rationale

edit

The decision whether to declare a task in the specification or body of an enclosing package is not a simple one. There are good arguments for both.

Hiding a task specification in a package body and exporting (via subprograms ) only required entries reduces the amount of extraneous information in the package specification. It allows your subprograms to enforce any order of entry calls necessary to the proper operation of the tasks. It also allows you to impose defensive task communication practices (see Guideline 6.2.2) and proper use of conditional and timed entry calls. Finally, it allows the grouping of entries into sets for export to different classes of users (e.g., producers versus consumers) or the concealment of entries that should not be made public at all (e.g., initialization, completion, signals). Where performance is an issue and there are no ordering rules to enforce, the entries can be renamed as subprograms to avoid the overhead of an extra procedure call.

An argument, which can be viewed as an advantage or disadvantage, is that hiding the task specification in a package body hides the fact of a tasking implementation from the user. If the application is such that a change to or from a tasking implementation or a reorganization of services among tasks need not concern users of the package, then this is an advantage. However, if the package user must know about the tasking implementation to reason about global tasking behavior, then it is better not to hide the task completely. Either move it to the package specification or add comments stating that there is a tasking implementation, describing when a call may block, etc. Otherwise, it is the package implementor's responsibility to ensure that users of the package do not have to concern themselves with behaviors such as deadlock, starvation, and race conditions.

Finally, keep in mind that hiding tasks behind a procedural interface prevents the usage of conditional and timed entry calls and entry families, unless you add parameters and extra code to the procedures to make it possible for callers to direct the procedures to use these capabilities.

Exceptions

edit

This section addresses the issue of exceptions in the context of program structures. It discusses how exceptions should be used as part of the interface to a unit, including what exceptions to declare and raise and under what conditions to raise them. Information on how to handle, propagate, and avoid raising exceptions is found in Guideline 5.8. Guidelines on how to deal with portability issues are in Guideline 7.5.

Using Exceptions to Help Define an Abstraction

edit

guideline

edit
  • For unavoidable internal errors for which no user recovery is possible, declare a single user-visible exception. Inside the abstraction, provide a way to distinguish between the different internal errors.
  • Do not borrow an exception name from another context.
  • Export (declare visibly to the user) the names of all exceptions that can be raised.
  • In a package, document which exceptions can be raised by each subprogram and task entry.
  • Do not raise exceptions for internal errors that can be avoided or corrected within the unit.
  • Do not raise the same exception to report different kinds of errors that are distinguishable by the user of the unit.
  • Provide interrogative functions that allow the user of a unit to avoid causing exceptions to be raised.
  • When possible, avoid changing state information in a unit before raising an exception.
  • Catch and convert or handle all predefined and compiler-defined exceptions at the earliest opportunity.
  • Do not explicitly raise predefined or implementation-defined exceptions.
  • Never let an exception propagate beyond its scope.

example

edit

This package specification defines two exceptions that enhance the abstraction:

-------------------------------------------------------------------------
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;
--------------------------------------------------------------------
...

rationale

edit

Exceptions should be used as part of an abstraction to indicate error conditions that the abstraction is unable to prevent or correct. Because the abstraction is unable to correct such an error, it must report the error to the user. In the case of a usage error (e.g., attempting to invoke operations in the wrong sequence or attempting to exceed a boundary condition), the user may be able to correct the error. In the case of an error beyond the control of the user, the user may be able to work around the error if there are multiple mechanisms available to perform the desired operation. In other cases, the user may have to abandon use of the unit, dropping into a degraded mode of limited functionality. In any case, the user must be notified.

Exceptions are a good mechanism for reporting such errors because they provide an alternate flow of control for dealing with errors. This allows error-handling code to be kept separate from the code for normal processing. When an exception is raised, the current operation is aborted and control is transferred directly to the appropriate exception handler.

Several of the guidelines above exist to maximize the ability of the user to distinguish and correct different kinds of errors. Declaring new exception names, rather than raising exceptions declared in other packages, reduces the coupling between packages and also makes different exceptions more distinguishable. Exporting the names of all exceptions that a unit can raise, rather than declaring them internally to the unit, makes it possible for users of the unit to refer to the names in exception handlers. Otherwise, the user would be able to handle the exception only with an others handler. Finally, use comments to document exactly which of the exceptions declared in a package can be raised by each subprogram or task entry making it possible for the user to know which exception handlers are appropriate in each situation.

In situations where there are errors for which the abstraction user can take no intelligent action (e.g., there is no workaround or degraded mode), it is better to export a single internal error exception. Within the package, you should consider distinguishing between the different internal errors. For instance, you could record or handle different kinds of internal error in different ways. When you propagate the error to the user, however, you should use a special internal error exception, indicating that no user recovery is possible. You should also provide relevant information when you propagate the error, using the facilities provided in Ada.Exceptions. Thus, for any abstraction, you effectively provide N + 1 different exceptions: N different recoverable errors and one irrecoverable error for which there is no mapping to the abstraction. Both the application requirements and what the client needs/wants in terms of error information help you identify the appropriate exceptions for an abstraction.

Because they cause an immediate transfer of control, exceptions are useful for reporting unrecoverable errors, which prevent an operation from being completed, but not for reporting status or modes incidental to the completion of an operation. They should not be used to report internal errors that a unit was able to correct invisibly to the user.

To provide the user with maximum flexibility, it is a good idea to provide interrogative functions that the user can call to determine whether an exception would be raised if a subprogram or task entry were invoked. The function Stack_Empty in the above example is such a function. It indicates whether Underflow would be raised if Pop were called. Providing such functions makes it possible for the user to avoid triggering exceptions.

To support error recovery by its user, a unit should try to avoid changing state during an invocation that raises an exception. If a requested operation cannot be completely and correctly performed, then the unit should either detect this before changing any internal state information or should revert to the state at the time of the request. For example, after raising the exception Underflow, the stack package in the above example should remain in exactly the same state it was in when Pop was called. If it were to partially update its internal data structures for managing the stack, then future Push and Pop operations would not perform correctly. This is always desirable, but not always possible.

User-defined exceptions should be used instead of predefined or compiler-defined exceptions because they are more descriptive and more specific to the abstraction. The predefined exceptions are very general and can be triggered by many different situations. Compiler-defined exceptions are nonportable and have meanings that are subject to change even between successive releases of the same compiler. This introduces too much uncertainty for the creation of useful handlers.

If you are writing an abstraction, remember that the user does not know about the units you use in your implementation. That is an effect of information hiding. If any exception is raised within your abstraction, you must catch it and handle it. The user is not able to provide a reasonable handler if the original exception is allowed to propagate out of the body of your abstraction. You can still convert the exception into a form intelligible to the user if your abstraction cannot effectively recover on its own.

Converting an exception means raising a user-defined exception in the handler for the original exception. This introduces a meaningful name for export to the user of the unit. Once the error situation is couched in terms of the application, it can be handled in those terms.

Summary

edit

high-level structure

edit
  • Place the specification of each library unit package in a separate file from its body.
  • Avoid defining library unit subprograms that are not intended to be used as main programs. If such subprograms are defined, then create an explicit specification, in a separate file, for each library unit subprogram.
  • Minimize the use of subunits.
  • In preference to subunits, use child library units to structure a subsystem into manageable units.
  • Place each subunit in a separate file.
  • Use a consistent file naming convention.
  • In preference to nesting in a package body, use a private child and with it to the parent body.
  • Use private child unit specifications for data and subprograms that are required by (other) child units that extend a parent unit's abstraction or services.
  • When possible, express configuration pragmas through compiler options or other means that do not require modifications to the source code. .
  • When configuration pragmas must be placed in source code, consider isolating them to one compilation unit per partition; if specified, the main subprogram for the partition is recommended.
  • Use subprograms to enhance abstraction.
  • Restrict each subprogram to the performance of a single action.
  • Use a function when the subprogram's primary purpose is to provide a single value.
  • Minimize the side effect of a function.
  • Consider using a parameterless function when the value does not need to be static.
  • Use a parameterless function (instead of a constant) if the value should be inherited by types derived from the type.
  • Use a parameterless function if the value itself is subject to change.
  • Use packages for information hiding.
  • Use packages with tagged types and private types for abstract data types.
  • Use packages to model abstract entities appropriate to the problem domain.
  • Use packages to group together related type and object declarations (e.g., common declarations for two or more library units).
  • Encapsulate machine dependencies in packages. Place a software interface to a particular device in a package to facilitate a change to a different device.
  • Place low-level implementation decisions or interfaces in subprograms within packages.
  • Use packages and subprograms to encapsulate and hide program details that may change (Nissen and Wallis 1984).
  • If a new library unit represents a logical extension to the original abstraction, define it as a child library unit.
  • If a new library unit is independent (e.g., introduces a new abstraction that depends only in part on the existing one), then encapsulate the new abstraction in a separate library unit.
  • Use child packages to implement a subsystem.
  • Use public child units for those parts of a subsystem that should be visible to clients of the subsystem.
  • Use private child units for those parts of a subsystem that should not be visible to clients of the subsystem.
  • Use private child units for local declarations used only in implementing the package specification.
  • Use child packages to implement constructors, even when they return access values.
  • Make each package serve a single purpose.
  • Use packages to group related data, types, and subprograms.
  • Avoid collections of unrelated objects and subprograms (NASA 1987; Nissen and Wallis 1984).
  • Consider restructuring a system to move two highly related units into the same package (or package hierarchy) or to move relatively independent units into separate packages.
  • Avoid declaring variables in package specifications.
  • Use tasks to model abstract, asynchronous entities within the problem domain.
  • Use tasks to define concurrent algorithms for multiprocessor architectures.
  • Use tasks to perform concurrent, cyclic, or prioritized activities (NASA 1987).
  • Use protected types to control or synchronize access to data or devices.
  • Use protected types to implement synchronization tasks, such as a passive resource monitor.

visibility

edit
  • Put only what is needed for the use of a package into its specification.
  • Minimize the number of declarations in package specifications.
  • Do not include extra operations simply because they are easy to build.
  • Minimize the context (with) clauses in a package specification.
  • Reconsider subprograms that seem to require large numbers of parameters.
  • Do not manipulate global data within a subprogram or package merely to limit the number of parameters.
  • Avoid unnecessary visibility; hide the implementation details of a program unit from its users.
  • Use child library units to control the visibility of parts of a subsystem interface.
  • Use private child packages for those declarations that should not be used outside the subsystem.
  • Use child library units to present different views of an entity to different clients.
  • Design (and redesign) interfaces after having worked out the logic of various expected clients of the interface.
  • Use child packages rather than nested packages to present different views of the same abstraction.
  • Nest package specifications within another package specification only for grouping operations or hiding common implementation details.
  • Consider using private child packages in lieu of nesting.
  • Restrict the visibility of program units as much as possible by nesting them inside package bodies (Nissen and Wallis 1984) if you cannot use a private child package.
  • Minimize nesting program units inside subprograms and tasks.
  • Minimize the scope within which with clauses apply.
  • Only with those units directly needed.
  • Carefully consider encapsulation of tasks.

exceptions

edit
  • For unavoidable internal errors for which no user recovery is possible, declare a single user-visible exception. Inside the abstraction, provide a way to distinguish between the different internal errors.
  • Do not borrow an exception name from another context.
  • Export (declare visibly to the user) the names of all exceptions that can be raised.
  • In a package, document which exceptions can be raised by each subprogram and task entry.
  • Do not raise exceptions for internal errors that can be avoided or corrected within the unit.
  • Do not raise the same exception to report different kinds of errors that are distinguishable by the user of the unit.
  • Provide interrogative functions that allow the user of a unit to avoid causing exceptions to be raised.
  • When possible, avoid changing state information in a unit before raising an exception.
  • Catch and convert or handle all predefined and compiler-defined exceptions at the earliest opportunity.
  • Do not explicitly raise predefined or implementation-defined exceptions.
  • Never let an exception propagate beyond its scope.

Concurrency