Ada 95 Quality and Style Guide Chapter 9
CHAPTER 9
Object-Oriented Features
This chapter recommends ways of using Ada's object-oriented features.
Ada supports inheritance and polymorphism, providing the programmer
some effective techniques and building blocks. Disciplined use
of these features will promote programs that are easier to read
and modify. These features also give the programmer flexibility
in building reusable components.
The following definitions are provided in order to make this chapter
more understandable. The essential characteristics of object-oriented
programming are encapsulation, inheritance, and polymorphism.
These are defined as follows in the Rationale (1995, §§4.1
and III.1.2):
As stated in the Ada Reference Manual (1995, Annex N):
9.1 OBJECT-ORIENTED DESIGN
You will find it easier to take advantage of many of the concepts
in this chapter if you have done an
object-oriented design. The results of an object-oriented design
would include a set of meaningful abstractions and hierarchy of
classes. The abstractions need to include the definition of the
design objects, including structure and state, the operations
on the objects, and the intended encapsulation for each object.
The details on designing these abstractions and the hierarchy
of classes are beyond the scope of this book. A number of good
sources exist for this detail, including Rumbaugh et al. (1991),
Jacobson et al. (1992), Software Productivity Consortium (1993),
and Booch (1994).
An important part of the design process is deciding on the overall
organization of the system. Looking at a single type, a single
package, or even a single class of types by itself is probably
the wrong place to start. The appropriate level to start is more
at the level of "subsystem" or "framework."
You should use child packages (Guidelines 4.1.1 and 4.2.2) to
group sets of abstractions into subsystems representing reusable
frameworks. You should distinguish the "abstract" reusable
core of the framework from the particular "instantiation"
of the framework. Presuming the framework is constructed properly,
the abstract core and its instantiation can be separated into
distinct subsystems within the package hierarchy because the internals
of an abstract reusable framework probably do not need to be visible
to a particular instantiation of the framework.
9.2 TAGGED TYPE HIERARCHIES
You should use inheritance primarily as a mechanism for implementing
a class hierarchy from an object-oriented design. A
class hierarchy should be a generalization/specialization ("is-a")
relationship. This relationship may also be referred to as "is-a-kind-of,"
not to be confused with "is an instance of." This "is-a"
usage of inheritance is in contrast to other languages in which
inheritance is used also to provide the equivalent of the Ada
context clauses with and use. In Ada, you first
identify the external modules of interest via with clauses
and then choose selectively whether to make only the name of the
module (package) visible or its contents (via a use clause).
9.2.1 Tagged Types
guideline
- Consider using type extension when designing an is-a (generalization/specialization)
hierarchy.
- Use tagged types to preserve a common interface across differing
implementations (Taft 1995a).
- When defining a tagged type in a package, consider including
a definition of a general access type to the corresponding class-wide
type.
- In general, define only one tagged type per package.
example
rationale
exceptions
9.2.2 Properties of Dispatching Operations
guideline
- The implementation of the dispatching operations of
each type in a derivation class rooted in a tagged type T
should conform to the expected semantics of the corresponding
dispatching operations of the
class-wide type T'Class.
example
rationale
exceptions
9.2.3 Controlled Types
guideline
- Consider using a controlled type whenever
a type allocates resources that must be deallocated or otherwise
"cleaned up" on destruction or overwriting.
- Use a derivation from a controlled type in preference to providing
an explicit "cleanup" operation that must be called
by clients of the type.
- When overriding the adjustment and finalization procedures derived
from controlled types, define the finalization procedure to undo
the effects of the adjustment procedure.
- Derived type initialization procedures
should call the initialization procedure of their parent as part
of their type-specific initialization.
- Derived type finalization procedures should
call the finalization procedure of their parent as part of their
type-specific finalization.
- Consider deriving a data structure's components rather than
the enclosing data structure from a controlled type.
example
rationale
9.2.4 Abstract Types
guideline
- Consider using abstract types and operations
in creating classification schemes, for example, a taxonomy, in
which only the leaf objects will be meaningful in the application.
- Consider declaring root types and internal nodes in a type tree
as abstract.
- Consider using abstract types for generic formal
derived types.
- Consider using abstract types to develop different implementations
of a single abstraction.
example
rationale
notes
9.3 TAGGED TYPE OPERATIONS
You can use three options when you define the operations on a
tagged type and its descendants. These
categories are primitive abstract, primitive nonabstract, and
class-wide operations. An abstract operation must be overridden
for a nonabstract derived type. A nonabstract operation may be
redefined for a subclass. A class-wide operation cannot
be overridden by a subclass definition. A class-wide operation
can be redefined for the derivation class rooted in the derived
type; however, this practice is discouraged because of the ambiguities
it introduces in the code.
Through careful usage of these options, you can ensure that your
abstractions preserve class-wide properties, as discussed in Guideline
9.2.1. As stated above, this principle requires that any type
that is visibly derived from some parent type must fully support
the semantics of the parent type.
9.3.1 Primitive Operations and Redispatching
guideline
- Consider declaring a primitive abstract operation based on the
absence of a meaningful "default" behavior.
- Consider declaring a primitive nonabstract operation based on
the presence of a meaningful "default" behavior.
- When overriding an operation, the overriding subprogram should
not raise exceptions that are not known to the users of the overridden
subprogram.
- If redispatching is used in the implementation of the operations
of a type, with the specific intent that some of the redispatched-to
operations be overridden by specializations for the derived types,
then document this intent clearly in the specification as part
of the "interface" of a parent type with its derived
types.
- When redispatching is used (for any reason) in the implementation
of a primitive operation of a tagged type, then document (in some
project-consistent way) this use in the body of the operation
subprogram so that it can be easily found during maintenance.
example
rationale
9.3.2 Class-Wide Operations
guideline
- Consider using a class-wide operation (i.e.,
an operation with parameter[s] of a class-wide type) when an operation
can be written, compiled, and tested without knowing all the possible
descendants of a given tagged type (Barnes 1996).
- Consider using a class-wide operation when you do not want an
operation to be inherited and/or overridden.
example
rationale
9.3.3 Constructors
Ada does not define a unique syntax for constructors. In
Ada a constructor for a type is defined as an operation that produces
as a result a constructed object, i.e., an initialized instance
of the type.
guideline
- Avoid declaring a constructor as a primitive abstract operation.
- Use a primitive abstract operation to declare an initialization
function or constructor only when objects
of the inheriting derived types will not require additional parameters
for initialization.
- Consider using access discriminants
to provide parameters to default initialization.
- Use constructors for explicit initialization.
- Consider splitting the initialization and construction of an
object.
- Consider declaring a constructor operation in a child package.
- Consider declaring a constructor operation to return an access
value to the constructed object
(Dewar 1995).
example
rationale
notes
9.3.4 Equality
guideline
- When you redefine the "=" operator on a tagged
type, make sure that it has the expected behavior in extensions
of this type and override it if necessary.
example
rationale
9.3.5 Polymorphism
guideline
- Consider using class-wide programming to provide run-time, dynamic
polymorphism when constructing larger, reusable, extensible frameworks.
- When possible, use class-wide programming rather than variant
records.
- Use class-wide programming to provide a consistent interface
across the set of types in the tagged type hierarchy (i.e., class).
- Consider using generics to define a new type in terms of an
existing type, either as an extension or as a container, collection,
or composite data structure.
- Avoid using type extensions for parameterized abstractions when
generics provide a more appropriate mechanism.
example
rationale
9.4 MANAGING VISIBILITY
9.4.1 Derived Tagged Types
guideline
- Consider giving derived tagged types the same visibility to
the parent type as other clients of the parent.
- Define a derived tagged type in a child of the package that
defines the base type if the implementation of the derived type
requires greater visibility into the implementation of the base
type than other clients of the base type require.
example
rationale
9.5 MULTIPLE INHERITANCE
Ada provides several mechanisms to support multiple inheritance,
where multiple inheritance is a means for incrementally building
new abstractions from existing ones, as defined at the beginning
of this chapter. Specifically, Ada supports multiple inheritance
module inclusion (via multiple with/use clauses),
multiple inheritance "is-implemented-using" via private
extensions and record composition, and multiple inheritance mixins
via the use of generics, formal packages, and access discriminants
(Taft 1994).
9.5.1 Multiple Inheritance Techniques
guideline
- Consider using type composition for implementation, as opposed
to interface, inheritance.
- Consider using a generic to "mix in"
functionality to a derivative of some core abstraction.
- Consider using access discriminants to support "full"
multiple inheritance where an object must be referenceable as
an entity of two or more distinct unrelated abstractions.
example
rationale
9.6 SUMMARY
tagged type hierarchies
- Consider using type extension when designing an is-a (generalization/specialization)
hierarchy.
- Use tagged types to preserve a common interface across differing
implementations (Taft 1995a).
- When defining a tagged type in a package, consider including
a definition of a general access type to the corresponding class-wide
type.
- In general, define only one tagged type per package.
- The implementation of the dispatching operations of
each type in a derivation class rooted in a tagged type T
should conform to the expected semantics of the corresponding
dispatching operations of the
class-wide type T'Class.
- Consider using a controlled type whenever
a type allocates resources that must be deallocated or otherwise
"cleaned up" on destruction or overwriting.
- Use a derivation from a controlled type in preference to providing
an explicit "cleanup" operation that must be called
by clients of the type.
- When overriding the adjustment and finalization procedures derived
from controlled types, define the finalization procedure to undo
the effects of the adjustment procedure.
- Derived type initialization procedures
should call the initialization procedure of their parent as part
of their type-specific initialization.
- Derived type finalization procedures should
call the finalization procedure of their parent as part of their
type-specific finalization.
- Consider deriving a data structure's components rather than
the enclosing data structure from a controlled type.
- Consider using abstract types and operations
in creating classification schemes, for example, a taxonomy, in
which only the leaf objects will be meaningful in the application.
- Consider declaring root types and internal nodes in a type tree
as abstract.
- Consider using abstract types for generic formal
derived types.
- Consider using abstract types to develop different implementations
of a single abstraction.
tagged type operations
- Consider declaring a primitive abstract operation based on the
absence of a meaningful "default" behavior.
- Consider declaring a primitive nonabstract operation based on
the presence of a meaningful "default" behavior.
- When overriding an operation, the overriding subprogram should
not raise exceptions that are not known to the users of the overridden
subprogram.
- If redispatching is used in the implementation of the operations
of a type, with the specific intent that some of the redispatched-to
operations be overridden by specializations for the derived types,
then document this intent clearly in the specification as part
of the "interface" of a parent type with its derived
types.
- When redispatching is used (for any reason) in the implementation
of a primitive operation of a tagged type, then document (in some
project-consistent way) this use in the body of the operation
subprogram so that it can be easily found during maintenance.
- Consider using a class-wide operation (i.e.,
an operation with parameter[s] of a class-wide type) when an operation
can be written, compiled, and tested without knowing all the possible
descendants of a given tagged type (Barnes 1996).
- Consider using a class-wide operation when you do not want an
operation to be inherited and/or overridden.
- Avoid declaring a constructor as a primitive abstract operation.
- Use a primitive abstract operation to declare an initialization
function or constructor only when objects
of the inheriting derived types will not require additional parameters
for initialization.
- Consider using access discriminants
to provide parameters to default initialization.
- Use constructors for explicit initialization.
- Consider splitting the initialization and construction of an
object.
- Consider declaring a constructor operation in a child package.
- Consider declaring a constructor operation to return an access
value to the constructed object
(Dewar 1995).
- When you redefine the "=" operator on a tagged
type, make sure that it has the expected behavior in extensions
of this type and override it if necessary.
- Consider using class-wide programming to provide run-time, dynamic
polymorphism when constructing larger, reusable, extensible frameworks.
- When possible, use class-wide programming rather than variant
records.
- Use class-wide programming to provide a consistent interface
across the set of types in the tagged type hierarchy (i.e., class).
- Consider using generics to define a new type in terms of an
existing type, either as an extension or as a container, collection,
or composite data structure.
- Avoid using type extensions for parameterized abstractions when
generics provide a more appropriate mechanism.
managing visibility
- Consider giving derived tagged types the same visibility to
the parent type as other clients of the parent.
- Define a derived tagged type in a child of the package that
defines the base type if the implementation of the derived type
requires greater visibility into the implementation of the base
type than other clients of the base type require.
multiple inheritance
- Consider using type composition for implementation, as opposed
to interface, inheritance.
- Consider using a generic to "mix in"
functionality to a derivative of some core abstraction.
- Consider using access discriminants to support "full"
multiple inheritance where an object must be referenceable as
an entity of two or more distinct unrelated abstractions.