Ada 95 Quality and Style Guide Chapter 4
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.
example
This package specification defines two exceptions that enhance
the abstraction:
rationale
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.
visibility
exceptions
4.1 HIGH-LEVEL STRUCTURE
4.2 VISIBILITY
4.3 EXCEPTIONS
-------------------------------------------------------------------------
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;
--------------------------------------------------------------------
...
4.4 SUMMARY