Ada 95 Quality and Style Guide Chapter 4
CHAPTER 4
Program Structure
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.
4.1 HIGH-LEVEL 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.
4.1.1 Separate Compilation Capabilities
guideline
- 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
rationale
4.1.2 Configuration Pragmas
guideline
- 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
exceptions
4.1.3 Subprograms
guideline
- Use subprograms to enhance
abstraction.
- Restrict each subprogram to the performance of a single action.
example
rationale
notes
4.1.4 Functions
guideline
- 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
rationale
4.1.5 Packages
guideline
- 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
rationale
notes
4.1.6 Child Library UnitsSee
guideline
- 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
rationale
4.1.7 Cohesion
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; 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
rationale
notes
4.1.8 Data Coupling
guideline
- Avoid declaring variables in package
specifications.
example
rationale
notes
4.1.9 Tasks
guideline
- 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
4.1.10 Protected Types
guideline
- 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
rationale
4.2 VISIBILITY
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.
4.2.1 Minimization of Interfaces
guideline
- 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 See alsoto
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
rationale
notes
4.2.2 Nested Packages
guideline
- 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
rationale
4.2.3 Restricting Visibility
guideline
- 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
rationale
notes
4.2.4 Hiding Tasks
guideline
- Carefully consider encapsulation of tasks.
example
rationale
4.3 EXCEPTIONS
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.
4.3.1 Using Exceptions to Help Define an Abstraction
guideline
- 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
rationale
4.4 SUMMARY
high-level structure
- 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
- 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
- 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.