A generic class is one that can produce many types of object, depending on the parameter passed to it. The code in the class is genric; it makes no assumption about the type or structure of its parameter, and so works for any type of parameter. The simplest generic classes are arrays and lists. A generic class may be used as a supplier, or as a parent class. The type of parameter passed to a generic class can be constrained in the generic class header, so a constrained generic class only accepts a parameter that conforms to the constraint.
The classes ARRAY and LINKED_LIST are generic classes, because they can use objects of any type. A generic class is passed an actual parameter in the variable declaration, and the actual parameter is bound to the formal parameter when the system is compiled. The single class ARRAY, for example, can generate many types of objects, such as ARRAY [INTEGER], ARRAY [BANK], and so on. The class header for the class ARRAY is written so that it can receive a formal parameter:
class ARRAY [T]
-- define the features for an array of objects of type T
In the class definition, formal parameters are written after the name of the class, enclosed in square brackets; multiple parameters are separated by commas. The names "T" ( for Type) and "G" (for Generic) are common names for formal parameters. Parameter binding occurs at compile time, where argument binding (in routines) occurs at run-time. Any number of parameters may be defined in the header, separated by commas; one such class header is
class COMPLICATED [A, B, C, D]
-- some compilcated class that receives four actual parameters
The code within the class is generic: it makes no assumption about the type or structure of the parameter, and simply stores and retrieves whole objects without using any feature of these objects. This is done via the formal parameter T, which is used in the features of the class and at run-time is of whatever type was passed as a parameter. The feature item in class ARRAY, for example, has the feature header
item (i: INTEGER): T
-- entry at index I, if in index interval
so it returns an object of the type bound to T when the code was compiled.
The most common way to use a generic class is as a supplier class, where the client declares an array of customers (say):
class BANK
creation
make
feature
customers: ARRAY [CUSTOMER]
Code in class BANK creates the array, stores customers in it, and retrieves customers from it.
A generic class may be inherited, just like any other class. It may be inherited with the formal parameters bound, or unbound. It is often simpler to inherit a generic class, than to use a generic class as a client. A bank, for example, may be defined to contain a list of customers, so the code in class BANK could be
class BANK
creation
make
feature
customers: LINKED_LIST [CUSTOMER]
...
On the other hand, a bank may be seen as a list of customers, so long as there is only a single list; the bank is a bunch of customers. The code in class BANK would then look like
class BANK
inherit
LINKED_LIST [CUSTOMER]
creation
make
and a client would create the bank using the inherited list creation routine make, and could then use all the list features on the bank
class CLIENT
creation
make
feature
bank: BANK
make is
do
!!bank.make
from bank.start
until bank.after
...
As always, the child class (BANK) can add new features that are specific to the child.
The inheritance chart for class BANK in this example is shown below. Note that the right arrow here indicates parameter passing, not the usual client declaration, so it can be added to an inheritance chart; this is not a client relation. A separate client chart is needed to show the class BANK as a supplier or as a client.
A user can define a new generic class simply by writing the appropriate generic class header and code. A generic class is defined by giving a formal parameter in the class header. To define a storable list, for example, we would use the code
class STORE_LIST [T]
inherit
STORABLE
LINKED_LIST[T]
creation
make
end -- class STORE_ARRAY
The class STORE_ARRAY is generic, because it can handle any type of object passed as an actual parameter from a client. An actual parameter is passed from a client, and bound to all occurrences of the formal parameter, so the client code to create a storable list of customers would be something like
class CLIENT
creation
make
feature
customers: STORE_LIST [CUSTOMER]
User-defined generic classes are shown on client and inheritance charts like any generic class. The formal parameter is shown after the class name, with a right arrow to the actual parameter.
A generic class is powerful because it makes no assumptions about the objects that it uses; formally, it makes no assumption except that the object is of type ANY. An array and a list manipulate complete objects and never "look inside" an object. There are many cases, however, where a generic class needs to use features of the object. An example of this is a list, where each element has a unique key. The key can be used to search the list and find the desired object, but such a comparison has to be done by the client of the list, and has to be done by every client of a list with a key. The list class cannot assume that all objects it uses have a unique key, because such an assumption is not true for generic lists.
A special class can be defined for a list of objects with unique keys, by inheriting the list class and adding facilities to check the key of each object in the list. Such a class, however, can only handle certain types of objects: those with a key field. The type of object that can be passed as a parameter to this class must be constrained to have a key. This can be done by constraining the parameter list so only certain types of objects can be passed as parameters.
A generic class can specify that only certain types should be used by the class, by placing a constraint on the type of the formal parameter. This technique is called constrained genericity, because the parameter type is constrained by the class header. Because the type of parameter is constrained, specialised classes can be defined that make use of the features of the constrained type. A constraint is defined on the formal parameter in the class header by stating that the formal parameter must be of some specific type; the only valid actual parameters are then classes of this type. The mechanism is more general than it might at first appear, because any actual parameter that conforms to the constraint is valid.
A generic class header is constrained by listing the formal parameter, the constraint indicator "->", and the name of the constraining class. The form of a constrained class header is
class G [T -> C]
which means "In the generic class G, the class bound to T must be of type C".
Consider the class CUSTOMER in the BANK system. It would be nice to define a keyed list class that could search a list of customers, and look inside each element to see if it matched a given key value. Class CUSTOMER has to contain a key, and an exported routine match that receives a test key and returns true if the test key matches the key of the current object. The code in class CUSTOMER would look like
class CUSTOMER
feature
key: INTEGER
match (test: INTEGER): BOOLEAN is
-- does the current object match this key?
do
Result := test = key
end -- match
The constrained generic class, that is a list of keyed objects, may then be defined as
class KEY_LIST [T -> CUSTOMER]
inherit
LINKED_LIST [T]
creation
make
feature
find (target: INTEGER) is
-- position the cursor at the element with key value target
-- or after if no matching element is in the list
do
from start
until after or else item.match (target)
loop forth
end
end -- find
The name of the list is not contained in the code, because the code refers to the current object; the object is a list, because it inherits class LINKED_LIST. The code looks inside an object because it uses a feature called match that takes an integer and returns a boolean value, so the class must use constrained genericity.
To use the power of matching arbitrary keys, the constraint in the class header is loosened so that any keyed class can be passed as a parameter. This constraint can be enforced by using the inheritance hierarchy to define a deferred class MATCHABLE that contains a deferred feature match, and making this the constraint. A CUSTOMER can then inherit MATCHABLE and effect the deferred routine.
A client of class KEY_LIST would then create and use the object of type KEY_LIST [T]. A bank might use the keyed list of customers as a client with the code
class BANK
creation
make
feature
customers: KEY_LIST [CUSTOMER]
The inheritance (top) and client (bottom) structure for this system is shown below.
The code for such a system could look like
deferred class MATCHABLE
feature
match (target: INTEGER): BOOLEAN is
-- does the key match the target?
deferred
end -- match
end -- class MATCHABLE
class CUSTOMER
inherit
MATCHABLE;
PERSON
feature
match (target: INTEGER): BOOLEAN is
-- does the key match the target?
do
Result := key = target
end -- match
...
end -- class CUSTOMER
class KEY_LIST [T -> MATCHABLE]
inherit
LINKED_LIST [T]
creation
make
feature
find (target: INTEGER) is
-- position the cursor at the element with key value target
-- or off-_right if no matching element is in the list
do
from start
until after or else item.match(target)
loop forth
end
end -- find
found: BOOLEAN is
-- was the target found?
do
Result := not after
end -- found
end -- class KEY_LIST
The general mechanism for constrained genericity can thus be defined in four steps:
1. Define a deferred parent (MATCHABLE
here)
2. Inherit the parent class and effect the
deferred feature (match in class CUSTOMER here)
3. List the parent as the generic constraint
(KEY_LIST here)
4. Pass the base class (CUSTOMER here)
as the actual parameter
Warning: The rules of conformance for an expanded type (Section 11.2) say that an expanded type conforms to objects of the expanded type itself, and to objects of its base (reference) type. This places a restriction on the type of the argument that can be sent to a match routine in the example above.
The most general header for a match routine is
match (target: ANY): BOOLEAN is ...
because any reference type can be used as an actual argument. An actual argument of an expanded type, such as INTEGER, does not conform to the formal argument and thus cannot be sent as an actual argument to this match function.
A traditional programming language, such as Pascal or C, supports code reuse by routines. A routine is defined once and then called as needed, with any needed data being passed as arguments to the routine. Eiffel supports code reuse in four ways: routine, client, inherit, and generic.
An Eiffel routine is defined to do a single thing, and complex actions are achieved by calling a series of routines. A set of preconditions can be defined on each routine, to define the values that can be passed as arguments; if the precondition is true when the routine is called, then the routine guarantees the effect or postcondition of the routine.
The class provides the basic unit of reuse in OO programming, because it encapsulates a set of related data and routines. In Eiffel, both data and routines are treated equally as features of the class, so we can simply talk about the behaviour of the class. The use of a feature outside the class is explicitly controlled by the export policy; it is usual to export selected routines, and to hide the data inside the class. The simplest form of reuse is provided by the client relation, in which a client of the supplier class creates an object of that type, and then uses the services of that object. The creation of an object is controlled by the export policy on the creation routine.
A class can be reused by the inheritance relation. Inheritance occurs when one class can be viewed as a more specific version of another class; client means "has", "uses", or "contains" where inherit means "is". All features in a parent class are available to the inheriting, child class, and more features are usually added by each child. A feature may be defined in the parent class, or deferred until the child. When a feature is inherited, it can be used unchanged, effected, its name, signature, or body may be altered, and its export status may be altered in the child. A class may use single, multiple, or repeated inheritance.
A class can be reused by passing it to a generic class, that contains generic code to handle any object passed as a parameter to the class. If there is a need to use a feature of the object, then the inheritance hierarchy can be used to constrain the type of parameter passed to the constrained generic class. This allows type-specific code to be added to the new constrained class, so code can be written once inside the new class and used by a client of the generic class.
The price of reuse is a distributed system. In a traditional system, it is possible and common to write a long series of statements that are executed as a single chunk. This can produce very short and efficient code, but it does not support reuse. If any part of this code changes, then the whole routine has to be amended and recompiled. If a similar task needs to be done, then the existing code can offer a template for the new, similar code, but new code has to be written. If one part of the code needs to be done separately, then the existing code has to be re-written to separate out this part, or the code has to be written again in a separate routine.
The basic power of an OO system comes from three mechanisms. First, an object has its own data and shares the routines in its class, so we need not pass data around the system; call this data encapsulation. Second, a class contains the code that uses or chnages its data, and the class can be reused as an object, or as a parent; call this class encapsulation. Third, each feature in a class has a completely-defined interface, and the implementation is hidden inside the feature; call this code encapsulation.
The promise of an OO system is that code can be defined once, placed in the correct class, and then used forever. It is much harder and more time-consuming to write a good, reusable OO system than it is to write a procedural system, but the effort has to be made only once, not every time the system is changed.
13.6 Case study: constrained genericity
A keyed list is used for account and for customer
lookup.
Main points in this chapter
• A generic class handles objects of any type, passed as a parameter through the parameter list in the class header; such a process is called genericity. A generic class makes no assumptions about the behaviour of its objects.
• A generic class may be used as a client, or as a parent.
• A programmer can define a new generic class simply by writing a formal parameter in the class header, and then using it in the class code.
• Constrained genericity allows a generic class to use features of a parameter, by limiting or constraining the type of the parameter. A constraining parent class is defined, inherited by a child class, and listed as the constraint in the generic class. Only actual parameters of this type can then be passed.
• Procedural languages support code reuse by
routine call. Object-oriented languages support code reuse by routine call,
object creation, inheritance, and genricity.
1. Genericity gives one way of reusing code. How does it do this?
2. When are parameters bound? How many parameters can be passed to a generic class?
3. Why do we need constrained genericity? What does the word "constrained" mean? What is constrained? How is it constrained?
4. What are the main steps in constrained genericity ?
5. Build a constrained generic class:
a) Write code to display an ARRAY of POINTs.
b) Write code to display an ARRAY of PERSONs.
c) Write a generic class SHARRAY (show array)
that displays all items in the array.
d) Write the code that uses class SHARRAY to
replace (a) and (b)
6. List the ways that code is reused in Eiffel.