Ada 95 Quality and Style Guide Chapter 9

Chapter 9: Object-Oriented Features - TOC - 9.3 TAGGED TYPE OPERATIONS

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

    This example (Volan 1994) is intended to show a clean derivation of a square from a rectangle. You do not want to derive Square from Rectangle because Rectangle has semantics that are inappropriate for Square. (For instance, you can make a rectangle with any arbitrary height and width, but you should not be able to make a square this way.) Instead, both Square and Rectangle should be derived from some common abstract type, such as:

    Any_Rectangle:
    type Figure is abstract tagged
       record
          ...
       end record;
    type Any_Rectangle is abstract new Figure with private;
    -- No Make function for this; it's abstract.
    function Area (R: Any_Rectangle) return Float;
      -- Overrides abstract Area function inherited from Figure.
      -- Computes area as Width(R) * Height(R), which it will
      -- invoke via dispatching calls.
    function Width (R: Any_Rectangle) return Float is abstract;
    function Height (R: Any_Rectangle) return Float is abstract;
    type Rectangle is new Any_Rectangle with private;
    function Make_Rectangle (Width, Height: Float) return Rectangle;
    function Width (R: Rectangle) return Float;
    function Height (R: Rectangle) return Float;
    -- Area for Rectangle inherited from Any_Rectangle
    type Square is new Any_Rectangle with private;
    function Make_Square (Side_Length: Float) return Square;
    function Side_Length (S: Square) return Float;
    function Width (S: Square) return Float;
    function Height (S: Square) return Float;
    -- Area for Square inherited from Any_Rectangle
    ...
    -- In the body, you could just implement Width and Height for
    -- Square as renamings of Side_Length:
    function Width (S: Square) return Float renames Side_Length;
    function Height (S: Square) return Float renames Side_Length;
    function Area (R: Any_Rectangle) return Float is
    begin
      return Width(Any_Rectangle'Class(R)) * Height(Any_Rectangle'Class(R));
      -- Casting [sic, i.e., converting] to the class-wide type causes the function calls to
      -- dynamically dispatch on the 'Tag of R.
      -- [sic, i.e., redispatch on the tag of R.]
    end Area;
    

    Alternatively, you could just wait until defining types Rectangle and Square to provide actual Area functions:

    type Any_Rectangle is abstract new Figure with private;
    -- Inherits abstract Area function from Figure,
    -- but that's okay, Any_Rectangle is abstract too.
    function Width (R: Any_Rectangle) return Float is abstract;
    function Height (R: Any_Rectangle) return Float is abstract;
    type Rectangle is new Any_Rectangle with private;
    function Make_Rectangle (Width, Height: Float) return Rectangle;
    function Width (R: Rectangle) return Float;
    function Height (R: Rectangle) return Float;
    function Area (R: Rectangle) return Float; -- Overrides Area from Figure
    type Square is new Any_Rectangle with private;
    function Make_Square (Side_Length: Float) return Square;
    function Side_Length (S: Square) return Float;
    function Width (S: Square) return Float;
    function Height (S: Square) return Float;
    function Area (S: Square) return Float;  -- Overrides Area from Figure
    ...
    function Area (R: Rectangle) return Float is
    begin
      return Width(R) * Height(R); -- Non-dispatching calls
    end Area;
    function Area (S: Square) return Float is
    begin
      return Side_Length(S) ** 2;
    end Area;
    

    rationale

    The behavior of a nonabstract operation can be interpreted as the expected behavior for all members of the class; therefore, the behavior must be a meaningful default for all descendants. If the operation must be tailored based on the descendant abstraction (e.g., computing the area of a geometric shape depends on the specific shape), then the operation should be primitive and possibly abstract. The effect of making the operation abstract is that it guarantees that each descendant must define its own version of the operation. Thus, when there is no acceptable basic behavior, an abstract operation is appropriate because a new version of the operation must be provided with each derivation.

    All operations declared in the same package as the tagged type and following the tagged type's declaration but before the next type declaration are considered its primitive operations. Therefore, when a new type is derived from the tagged type, it inherits the primitive operations. If there are any operations that you do not want to be inherited, you must choose whether to declare them as class-wide operations (see Guideline 9.3.2) or to declare them in a separate package (e.g., a child package).

    Exceptions are part of the semantics of the class. By modifying the exceptions, you are violating the semantic properties of the class-wide type (see Guideline 9.2.1).

    There are (at least) two distinct users of a tagged type and its primitives. The "ordinary" user uses the type and its primitives without enhancement. The "extending" user extends the type by deriving a type based on the existing (tagged) type. Extending users and maintainers must determine the ramifications of a possibly incorrect extension. The guidelines here try to strike a balance between too much documentation (that can then easily get out of synch with the actual code) and an appropriate level of documentation to enhance the maintainability of the code.

    One of the major maintenance headaches associated with inheritance and dynamic binding relates to undocumented interdependencies among primitive (dispatching) operations of tagged types (the equivalent of "methods" in typical object-oriented terminology). If a derived type inherits some and overrides other primitive operations, there is the question of what indirect effects on the inherited primitives are produced. If no redispatching is used, the primitives may be inherited as "black boxes." If redispatching is used internally, then when inherited, the externally visible behavior of an operation may change, depending on what other primitives are overridden. Maintenance problems (here, finding and fixing bugs) occur when someone overrides incorrectly (on purpose or by accident) an operation used in redispatching. Because this overriding can invalidate the functioning of another operation defined perhaps several levels of inheritance up from the incorrect operation, it can be extremely difficult to track down.

    In the object-oriented paradigm, redispatching is often used to parameterize abstractions. In other words, certain primitives are intended to be overridden precisely because they are redispatching. These primitives may even be declared as abstract, requiring that they be overridden. Because they are redispatching, they act as "parameters" for the other operations. Although in Ada much of this parameterization can be done using generics, there are cases where the redispatching approach leads to a clearer object-oriented design. When you document the redispatching connection between the operations that are to be overridden and the operations that use them, you make the intended use of the type much clearer.

    Hence, any use of redispatching within a primitive should be considered part of the "interface" of the primitive, at least as far as any inheritor, and requires documentation at the specification level. The alternative (i.e., not providing such documentation in the specification) is to have to delve deep into the code of all the classes in the derivation hierarchy in order to map out the redispatching calls. Such detective work compromises the black-box nature of object-oriented class definitions. Note that if you follow Guideline 9.2.1 on preserving the semantics of the class-wide dispatching operations in the extensions of derived types, you will minimize or avoid the problems discussed here about redispatching.


    < Previous Page Search Contents Index Next Page >
    1 2 3 4 5 6 7 8 9 10 11
    TOC TOC TOC TOC TOC TOC TOC TOC TOC TOC TOC
    Appendix References Bibliography