Ada '83 Quality and Style:
Guidelines for Professional Programmers
CHAPTER 4: Program Structure
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.
guideline
- Place the specification of each library unit package in a separate
file from its body.
- Create an explicit specification, in a separate file, for each library unit subprogram.
- Use subunits for the bodies of large units which are nested in other units.
- Place each subunit in a separate file.
- Use a consistent file naming convention.
example
The file names below illustrate one possible file organization and associated
consistent naming convention. The library unit name is used for the body. A
trailing underscore indicates the specification, and any files containing
subunits use names constructed by separating the body name from the subunit
name with two underscores.
text_io_.ada -- the specification
text_io.ada -- the body
text_io__integer_io.ada -- a subunit
text_io__fixed_io.ada -- a subunit
text_io__float_io.ada -- a subunit
text_io__enumeration_io.ada -- a subunit |
rationale
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 which could have been avoided, severely slowing the development
and test phase of a project. This is why we suggest placing specifications of
all library units (nonnested units) in separate files from their bodies.
For the same reason, use subunits for large nested bodies, and put each
subunit in its own file. This makes it possible to modify the body of the one
nested unit without having to recompile any of the other units in the body.
This is recommended for large units because changes are more likely to occur
in large units than in small ones.
An additional benefit of using multiple separate files is that it allows
different implementers 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 which may result from following this guideline.
Language Ref Manual references:
6.1 Subprogram Declarations,
7.2 Package Specifications and Declarations,
7.3 Package Bodies,
10.1 Compilation Units - Library Units,
10.2 Subunits of Compilation Units,
10 Program Structure and Compilation Issues,
F Implementation-Dependent Characteristics
guideline
- Use subprograms to enhance abstraction.
- Restrict each subprogram to the performance of a single action.
example
Your program is required to output text to many types of devices. Because the
devices would accept a variety of character sets, the proper way to do this is
to write a subprogram to convert to the required character set. This way, the
output subprogram has one purpose and the conversions are described elsewhere.
...
----------------------------------------------------------------------
procedure Dispatch_To_Device
(Output : in Text;
Device : in Device_Name;
Status : out Error_Codes) is
Upper_Case_Output : Text (1 .. Output'Length);
...
begin -- Dispatch_To_Device
...
case Device.Character_Set is
when Limited_ASCII =>
Convert_To_Upper_Case(Original => Output,
Upper_Case => Upper_Case_Output);
...
when Extended_ASCII =>
...
when EBCDIC =>
...
end case; -- Device_Type.Character_Set
...
end Dispatch_To_Device;
---------------------------------------------------------------------- |
rationale
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.
note
Dealing with the overhead of subroutine calls is discussed in Guideline 9.1.1.
Language Ref Manual references:
6 Subprograms
guideline
- Use a function when the subprogram's primary purpose is to provide a
single value.
- Minimize the side effect of a function.
example
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 should 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 is 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.
rationale
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 which depends on the order in which side
effects of functions occur is erroneous. Avoid using side effects anywhere.
Language Ref Manual references:
6.4 Subprogram Calls,
6.5 Function Subprograms,
8.3 Visibility
guideline
- Use packages for information hiding.
- Use packages with 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).
- Use packages to group together related program units for configuration
control or visibility reasons (NASA 1987).
- 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
A package called Backing_Storage_Interface
could contain type and subprogram
declarations to support a generalized view of an external memory system (such
as a disk or drum). Its internals may, in turn, depend on other packages more
specific to the hardware or operating system.
rationale
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.
note
The most prevalent objection to this guideline usually involves performance
penalties. See Guideline 9.1.1 for a discussion about subprogram overhead.
Language Ref Manual references:
3 Declarations and Types,
6.1 Subprogram Declarations,
7 Packages,
8.3 Visibility,
13 Representation Clauses and Implementation-Dependent Features,
B Predefined Language Pragmas
guideline
- 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 and
Nissen and Wallis 1984).
example
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 which provide an interface to a special purpose device.
rationale
See also Guideline 5.4.1 on Heterogeneous Data.
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 paraphrased).
note
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.
Language Ref Manual references:
3.2 Objects and Named Numbers,
3.3 Types and Subtypes,
6.1 Subprogram Declarations,
7 Packages
guideline
- Avoid declaring variables in package specifications.
example
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_Range is range 1 .. 2_500_000;
function Source_Line_Number return Line_Range;
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
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.
note
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 9.1.1 for a discussion about
subprogram overhead.
Language Ref Manual references:
3.2.1 Object Declarations,
7.2 Package Specifications and Declarations,
8.2 Scope of Declarations
guideline
- Use tasks to model abstract, asynchronous entities within the problem
domain.
- Use tasks to control or synchronize access to tasks or other
asynchronous entities (e.g., asynchronous I/O, peripheral devices, interrupts).
- Use tasks to define concurrent algorithms for multiprocessor
architectures.
- Use tasks to perform concurrent, cyclic, or prioritized activities
(NASA 1987).
rationale
The rationale for this guideline is given under Guideline 6.1.1. Chapter 6
discusses tasking in more detail.
Language Ref Manual references:
9 Tasks,
13.5.1 Interrupts
Back to document index