Skip to main content

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.

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 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

-------------------------------------------------------------------------
package Telephone_Book is
type Listing is limited private;
procedure Set_Name (New_Name : in String;
Current : in out Listing);
procedure Insert (Name : in String;
Current : in out Listing);
procedure Delete (Obsolete : in String;
Current : in out Listing);
private
type Information;
type Listing is access Information;
end Telephone_Book;
-------------------------------------------------------------------------
package body Telephone_Book is
-- Full details of record for a listing
type Information is
record
...
Next : Listing;
end record;
First : Listing;
procedure Set_Name (New_Name : in String;
Current : in out Listing) is separate;
procedure Insert (Name : in String;
Current : in out Listing) is separate;
procedure Delete (Obsolete : in String;
Current : in out Listing) is separate;
end Telephone_Book;
-------------------------------------------------------------------------

rationale

For each entity in the specification, give careful consideration to whether it could be moved to a child package or to the parent package body. The fewer the extraneous details, the more understandable the program, package, or subprogram. It is important to maintainers to know exactly what a package interface is so that they can understand the effects of changes. Interfaces to a subprogram extend beyond the parameters. Any modification of global data from within a package or subprogram is an undocumented interface to the "outside" as well.

Minimize the context clauses on a specification by moving unnecessary clauses to the body. This technique makes the reader's job easier, localizes the recompilation required when library units change, and helps prevent a ripple effect during modifications. See also Guideline 4.2.3.

Subprograms with large numbers of parameters often indicate poor design decisions (e.g., the functional boundaries of the subprogram are inappropriate or parameters are structured poorly). Conversely, subprograms with no parameters are likely to be accessing global data.

Objects visible within package specifications can be modified by any unit that has visibility to them. The object cannot be protected or represented abstractly by its enclosing package. Objects that must persist should be declared in package bodies. Objects whose value depends on program units external to their enclosing package are probably either in the wrong package or are better accessed by a subprogram specified in the package specification.

Child library units can provide distinct views of the hierarchical library. The engineer can provide a different view for the client than for the implementor (Rationale 1995, §10.1). By creating private child packages, the engineer can provide facilities that are only available inside the subsystem rooted at the parent library unit. The declarations inside a private child package specification are not exported outside the subsystem. Thus, the engineer can declare utilities needed to implement an abstraction in a private child package (e.g., debugging utilities [Cohen et al. 1993]) and be certain that users of the abstraction (i.e., the clients) cannot access these utilities.

Different clients may have different needs for essentially the same resource. Instead of having multiple versions of the resources, consider having child units that export different views for different purposes.

Designing an interface based strictly on predicting what clients "might" need can produce a bloated and inappropriate interface. What then happens is that clients try to "live" with the interface and work around the inappropriate interfaces, repeating code that logically should be part of the shared abstraction. See Guideline 8.3.1 for a discussion of interfaces from the reusability perspective.

notes

In some cases, subroutine libraries look like large, monolithic packages. In such cases, it may be beneficial to break these up into smaller packages, grouping them according to category (e.g., trigonometric functions).

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

Annex A of the Ada Reference Manual (1995) gives an example of package specification nesting. The specification of the generic package Generic_Bounded_Length is nested inside the specification of package Ada.Strings.Bounded. The nested package is a generic, grouping closely related operations.

rationale

Grouping package specifications into an encompassing package emphasizes a relationship of commonality among those packages. It also allows them to share common implementation details resulting from the relationship. Nesting packages allows you to organize the name space of the package in contrast to the semantic effect of nesting inside of subprograms or task bodies.

An abstraction occasionally needs to present different views to different classes of users. Building one view upon another as an additional abstraction does not always suffice because the functionality of the operations presented by the views may be only partially disjointed. Nesting specifications groups the facilities of the various views, yet associates them with the abstraction they present. Abusive mixing of the views by another unit would be easy to detect due to the multiple use clauses or an incongruous mix of qualified names.

See the rationale discussed in Guideline 4.2.1.

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

This program illustrates the use of child library units to restrict visibility. The procedure Rational_Numbers.Reduce is nested inside the body of Rational_Numbers to restrict its visibility to the implementation of this abstraction. Rather than make the text input/output facilities visible to the entire rational number hierarchy, it is only available to the body of the child library Rational_Numbers.IO. This example is adapted from the Ada Reference Manual (1995, §§7.1, 7.2, and 10.1.1):

-------------------------------------------------------------------------
package Rational_Numbers is
type Rational is private;
function "=" (X, Y: Rational) return Boolean;
function "/" (X, Y: Integer) return Rational; -- construct a rational number
function "+" (X, Y: Rational) return Rational;
function "-" (X, Y: Rational) return Rational;
function "*" (X, Y: Rational) return Rational;
function "/" (X, Y: Rational) return Rational; -- rational division
private
...
end Rational_Numbers;
package body Rational_Numbers is
procedure Reduce (R :in out Rational) is . . . end Reduce;
. . .
end Rational_Numbers;
package Rational_Numbers.IO is
procedure Put (R : in Rational);
procedure Get (R : out Rational);
end Rational_Numbers.IO;
with Ada.Text_IO;
with Ada.Integer_Text_IO;
package body Rational_Numbers.IO is -- has visibility to parent private type declaration
procedure Put (R : in Rational) is
begin
Ada.Integer_Text_IO.Put (Item => R.Numerator, Width => 0);
Ada.Text_IO.Put ("/");
Ada.Integer_Text_IO.Put (Item => R.Denominator, Width => 0);
end Put;
procedure Get (R : out Rational) is . . . end Get;
end Rational_Numbers.IO;

rationale

Restricting visibility of a program unit ensures that the program unit is not called from some part of the system other than that which was intended. This is done by nesting it inside the only unit that uses it, by hiding it inside a package body rather than declaring it in the package specification, or by declaring it as a private child unit. This avoids errors and eases the job of maintainers by guaranteeing that a local change in that unit will not have an unforeseen global effect.

Restricting visibility of a library unit by using with clauses on subunits rather than on the entire parent unit is useful in the same way. In the example above, it is clear that the package Text_IO is used only by the Listing_Facilities package of the compiler.

Nesting inside subprograms and tasks is discouraged because it leads to unreusable components. These components are essentially unreusable because they make undesirable up-level references into the defining context. Unless you truly want to ensure that the program unit is not called from some unintended part of the system, you should minimize this form of nesting.

See also Guideline 4.2.1 for a discussion of the use of child units.

notes

One way to minimize the coverage of a with clause is to use it only with subunits that really need it. Consider making those subunits separate compilation units when the need for visibility to a library unit is restricted to a subprogram or two.

Hiding Tasks

guideline

  • Carefully consider encapsulation of tasks.

example

-------------------------------------------------------------------------
package Disk_Head_Scheduler is
type Words is ...
type Track_Number is ...
procedure Transmit (Track : in Track_Number;
Data : in Words);
...
end Disk_Head_Scheduler;
-------------------------------------------------------------------------
package body Disk_Head_Scheduler is
...
task Control is
entry Sign_In (Track : in Track_Number);
...
end Control;
----------------------------------------------------------------------
task Track_Manager is
entry Transfer(Track_Number) (Data : in Words);
end Track_Manager;
----------------------------------------------------------------------
...
procedure Transmit (Track : in Track_Number;
Data : in Words) is
begin
Control.Sign_In(Track);
Track_Manager.Transfer(Track)(Data);
end Transmit;
----------------------------------------------------------------------
...
end Disk_Head_Scheduler;
-------------------------------------------------------------------------

rationale

The decision whether to declare a task in the specification or body of an enclosing package is not a simple one. There are good arguments for both.

Hiding a task specification in a package body and exporting (via subprograms ) only required entries reduces the amount of extraneous information in the package specification. It allows your subprograms to enforce any order of entry calls necessary to the proper operation of the tasks. It also allows you to impose defensive task communication practices (see Guideline 6.2.2) and proper use of conditional and timed entry calls. Finally, it allows the grouping of entries into sets for export to different classes of users (e.g., producers versus consumers) or the concealment of entries that should not be made public at all (e.g., initialization, completion, signals). Where performance is an issue and there are no ordering rules to enforce, the entries can be renamed as subprograms to avoid the overhead of an extra procedure call.

An argument, which can be viewed as an advantage or disadvantage, is that hiding the task specification in a package body hides the fact of a tasking implementation from the user. If the application is such that a change to or from a tasking implementation or a reorganization of services among tasks need not concern users of the package, then this is an advantage. However, if the package user must know about the tasking implementation to reason about global tasking behavior, then it is better not to hide the task completely. Either move it to the package specification or add comments stating that there is a tasking implementation, describing when a call may block, etc. Otherwise, it is the package implementor's responsibility to ensure that users of the package do not have to concern themselves with behaviors such as deadlock, starvation, and race conditions.

Finally, keep in mind that hiding tasks behind a procedural interface prevents the usage of conditional and timed entry calls and entry families, unless you add parameters and extra code to the procedures to make it possible for callers to direct the procedures to use these capabilities.