Previous |
Contents |
Next |
Profit comes from what is there,
usefulness from what is not there.
Lao Tsu, Tao Te Ching
The design techniques based on abstract data types which were discussed in the previous part allowed us to produce a fairly robust appointments diary and calculator which could be changed fairly easily to cope with changing requirements during the maintenance process. You may feel that all is now well with the world. To see how wrong you can be, consider another maintenance scenario. You have a working appointments diary and you need to adapt it to produce a new version to deal with meeting schedules. This means that in addition to the existing information that is kept about each appointment you need to maintain extra information for each appointment, such as a room number, an attendance list, an agenda and so on. How much new code does this involve writing? Can it be integrated into the existing diary system or will you end up with two separate systems, one for day-to-day use which doesnt record these details explicitly (although you could put them into the description of appointments) and another for meetings which will insist on you providing a room number and all the other extra bits and pieces? Putting room numbers into the descriptions of the day-to-day appointments may not be adequate if you want to find out what a particular room has been booked for or when it is booked; on the other hand for certain appointments (lunch dates etc.) the level of formality involved in a meetings diary might be excessive. Where are you having lunch, who with, whats the agenda?
This is quite a different sort of problem to the ones addressed in the preceding part; its about extensibility. Can you extend an existing system to cope with new requirements and still maintain compatibility with whats already there or do you have to rewrite everything? Can you produce a new version of the diary to cope with different types of appointments so that existing appointments are left unchanged (so that you dont have to specify spurious agendas for parties or lunch dates) while allowing new types of appointments to be integrated smoothly into whats already there?
To do this you would seem to need some sort of foresight which will allow you to predict the things you might need to change at some point in the future when you first design a system. This is where object-oriented programming languages like Ada 95 come into their own. They provide mechanisms for writing programs which will still work when confronted with undreamt-of variations on what they already do. It is yet another shift from a processor-centric view of the world as exemplified in the first part of this book towards a more data-centric view as described in the preceding part. Object-oriented programming languages take the data-centric view a step further: not only is the data concealed so that its structure can be altered if necessary, but the data directs its own processing. In the object-oriented view of the world, you do not perform operations on data items; instead, you ask the data items to perform operations on themselves, and different data items might perform these operations in quite distinct ways. If you want to extend the diary program to add a type of appointment, you arrange for the program to ask each appointment to perform its own operations so that any new appointment types operations get used automatically by the existing program.
This might not sound terribly revolutionary; after all, you saw in chapter 13 how you can build different versions of a stack. You can always push an item of data onto a stack, but in one case the stack in question might be implemented as an array and in another case it might be a linked list. You neither know nor care; all you know is that when you say Push! it pushes and when you say Pop! it pops. Different stacks might be performing the same operation in different ways; who knows, who cares, as long as it works? However, abstract data types as described thus far only really help with changes in the implementation of existing types; they dont cater for extending a program to cope with new data types, whereas the object-oriented techniques described in this part do.
Lets begin by considering what to do in order to extend the appointment type as it currently exists. Heres a package specification which is nearly identical to the appointment package in chapter 10:
with JE.Times; use JE.Times; package JE.Appointments is type Appointment_Type is private; function Date (Appt : Appointment_Type) return Time_Type; function Details (Appt : Appointment_Type) return String; procedure Appointment (Date : in Time_Type; Details : in String; Result : out Appointment_Type); procedure Put (Appt : in Appointment_Type); private type Appointment_Type is record Time : Time_Type; Details : String (1..50); Length : Natural := 0; end record; end JE.Appointments;
The only change is that the constructor function Appointment is now a procedure instead of a function, for reasons which Ill explain later, and Ive added a Put procedure to display an appointment on the screen. Now lets consider whats needed in order to produce a new appointment type for meetings which includes a room number. Other details apart from room numbers can be added in the same way, so Ill stick to just adding a room number in order to simplify matters. One way to add a room number would be to define a type called Meeting_Type which contains an appointment and a room number:
subtype Room_Type is Integer range 100 .. 999; type Meeting_Type is record Appt : Appointment_Type; Room : Room_Type; end record;
We can now declare a set of operations for Meeting_Type objects similar to those defined for Appointment_Type objects:
function Date (Meeting : Meeting_Type) return Time_Type; function Details (Meeting : Meeting_Type) return String; function Room (Meeting : Meeting_Type) return Room_Type; procedure Meeting (Date : in Time_Type; Details : in String; Room : in Room_Type; Result : out Meeting_Type); procedure Put (Meeting : in Meeting_Type);
These are the same as the operations for Appointment_Type except that Meeting (which constructs a Meeting_Type object from its components) has an extra parameter for the room number, and there is an extra function Room to extract the Room component of a meeting. Most of these operations are very similar to the equivalent operations for Appointment_Type; in fact, Date and Details will be identical. As an example, heres how Meeting and Date could be implemented:
procedure Meeting (Date : in Time_Type; Details : in String; Room : in Room_Type; Result : out Meeting_Type) is A : Appointment_Type; begin Appointment (Date, Details, A); Result := (Appt => A, Room => Room); end Meeting; function Date (Meeting : Meeting_Type) return Time_Type is begin return Date (Meeting.Appt); end Date;
Most of the work in these subprograms involves calling Appointment_Type operations to do the standard appointment-related work; any extra work involving the room number is then done as an afterthought.
Another approach is to use a record discriminant to merge the two record types into a single variant record, like this:
type Appointment_Kind is (Appointment, Meeting); type Appointment_Type (Kind : Appointment_Kind) is record Time : Time_Type; Details : String (1..50); Length : Natural := 0; case Kind is when Appointment => null; when Meeting => Room : Room_Type; end case; end record;
The record declaration consists of a fixed part which applies to all Appointment_Type objects, so that all Appointment_Type objects will have Date, Time, Details and Length components, followed by a variant part which looks very much like a case statement. What the variant part says is that when the Kind discriminant is set to Appointment, there will be no more components (as signified by null) but when Kind is set to Meeting you will also have a Room component. You can then declare objects of either variety like this:
A : Appointment_Type(Appointment); -- A has no Room component M : Appointment_Type(Meeting); -- M has a Room component
You can then process generalised Appointment_Types by using a case statement which inspects the discriminant and decides what to do in each case:
procedure Put (Appt : in Appointment_Type) is begin Put (Appt.Time); Put (Appt.Details(1..Appt.Length)); case Appt.Kind is when Appointment => null; -- do nothing for plain appointments when Meeting => Put (Appt.Room); -- display Room component for meetings end case; New_Line; end Put;
In this case the parameter to Put is unconstrained, which means that either variant of Appointment_Type can be passed as a parameter to Put. Inside Put, a case statement is used to select alternative courses of action depending on the value of the discriminant, which is what determines whether there is a Room component or not.
Although either of the approaches above will work, they are both fairly awkward ways of doing things. Putting an Appointment_Type inside a Meeting_Type involves defining a whole bunch of operations which are effectively just forwarding operations; each call is simply forwarded to the equivalent Appointment_Type operation where the real work gets done. All these new operations must still be tested, so there is a cost penalty for testing as well as development. Also, Meeting_Type and Appointment_Type are completely unrelated from the point of view of clients who cant see the full declaration of Meeting_Type; theres no easy way to convert an ordinary appointment to a meeting by tagging on a room number and you cant treat a meeting as an appointment by ignoring the room number. You would laboriously have to extract the date, time and details components and then put them back together again in order to perform a type conversion.
Using a variant record is simpler in some respects; you only have one copy of each subprogram. The disadvantage is that you have to know in advance what type variants you are going to support. Adding a new one involves modifying the original type declarations, and at every point where you discriminate between the variants you will have to add extra code to deal with the new variant. This will involve modifying, recompiling and retesting everything youve already written.
Fortunately, there is a much simpler way of defining extensible data types. A record type can be declared to be a tagged record which allows it to be extended later. Heres what the declaration of Appointment_Type would look like if wed defined it as a tagged record:
type Appointment_Type is tagged record Time : Time_Type; Details : String (1..50); Length : Natural := 0; end record;
The only difference is that the declaration says tagged record instead of just plain record. However, we can now define Meeting_Type by extending Appointment_Type like this:
type Meeting_Type is new Appointment_Type with record Room : Room_Type; end record;
This is another variation on derived types as described in chapter 5. Meeting_Type is derived from Appointment_Type; we say that Appointment_Type is Meeting_Types parent type. What the declaration of Meeting_Type says is that Meeting_Type is just like Appointment_Type except that it has an extra component called Room. A Meeting_Type object called M will therefore have five components called M.Date, M.Time, M.Details, M.Length and M.Room.
Bearing in mind that you can perform type conversions between derived types, you can convert M from a Meeting_Type to an Appointment_Type like this:
A : Appointment_Type := Appointment_Type (M);
All this does is to discard the extra components of M that were added to Appointment_Type when Meeting_Type was defined (which is just Room in this case). To convert the other way you have to use an extension aggregate to supply the missing components (namely, the value of Room):
M := (A with Room=>101);
This takes the value of A and adds a value of 101 for the room number to produce a Meeting_Type value.
More important is that, since Meeting_Type is derived from Appointment_Type, it inherits all the primitive operations of Appointment_Type (or primitive subprograms; the terms operation and subprogram are essentially synonymous in this context) just as a type derived from Integer would inherit all the primitive operations defined for Integer like "+", "" and so on. So what are the primitive operations of Appointment_Type? They are simply those operations on Appointment_Type values which were declared in the same package specification as Appointment_Type; in other words, any procedures or functions with an Appointment_Type parameter (a controlling parameter) as well as any functions which return an Appointment_Type result (a controlling result). Access parameters (as described in chapter 11) are also treated as controlling parameters, so you can also arrange for subprograms which need an access-to-Appointment_Type parameter to be primitive operations of type Appointment_Type.
Note that Appointment_Type must be declared in a package specification if it is to have any primitive operations; declaring a tagged type in a procedure and then declaring some operations on it doesnt mean that those operations are primitive operations of the type. Also, once a type like Meeting_Type is derived from Appointment_Type, you cant declare any more primitive operations for Appointment_Type (since otherwise this would mean allowing Meeting_Type to inherit operations which havent been declared yet):
package JE.Appointments is type Appointment_Type is tagged record ... end record; procedure X (Appt : in Appointment_Type); -- primitive, since it's in the same package -- specification as Appointment_Type type Meeting_Type is new Appointment_Type with ... ; procedure Y (Appt : in Appointment_Type); -- ILLEGAL! -- would be primitive, but not allowed since it -- follows the declaration of Meeting_Type end JE.Appointments;
The declaration of Appointment_Type is said to be frozen by the declaration of Meeting_Type; it would also be frozen if any Appointment_Type objects were declared. Once a type is frozen, you cannot declare any more primitive operations for it. The best way to avoid falling foul of the type-freezing rules is to declare all the primitive operations of a type immediately after the type declaration so that its obvious what the primitive operations of each type are.
The way inheritance works for primitive operations like X in the example above is that operations are implicitly declared immediately after the derived type declaration which are identical to the parent types primitive operations except that all uses of the parent types name in their specifications are effectively replaced by the name of the derived type. In the example above, Meeting_Type inherits a primitive operation called X; it is as if X were declared immediately after the declaration of Meeting_Type like this:
procedure X (Appt : in Meeting_Type);
Heres a revised version of JE.Appointments which declares Appointment_Type as a tagged type:
with JE.Times; use JE.Times; package JE.Appointments is type Appointment_Type is tagged private; function Date (Appt : Appointment_Type) return Time_Type; function Details (Appt : Appointment_Type) return String; procedure Appointment (Date : in Time_Type; Details : in String; Result : out Appointment_Type); procedure Put (Appt : in Appointment_Type); private type Appointment_Type is tagged record Time : Time_Type; Details : String (1..50); Length : Natural := 0; end record; end JE.Appointments;
Note that Appointment_Type is declared as tagged private in the visible part of the specification. Using tagged private reveals to clients of the package that Appointment_Type is a tagged type. Of course, the full declaration in the private part must be a tagged type as advertised so that the full view of the type has at least the same capabilities as the partial view given in the visible part; alternatively you can just declare the type as private in the visible part of the package if you dont want to let clients know whether the actual type is tagged or not.
We can now define Meeting_Type in a child package; this avoids having to modify the existing diary package and so avoids having to recompile all the diary packages clients. Remember that a child package is treated as an extension of its parent package and that the private part of the child (as well as the child package body) can use the information in the private part of its parent. Heres a specification for the child package:
package JE.Appointments.Meetings is subtype Room_Type is Integer range 100 .. 999; type Meeting_Type is new Appointment_Type with private; procedure Meeting (Date : in Time_Type; Details : in String; Room : in Room_Type; Result : out Meeting_Type); function Room (Appt : Meeting_Type) return Room_Type; private type Meeting_Type is new Appointment_Type with record Room : Room_Type; end record; end JE.Appointments.Meetings;
If Appointment_Type had simply been declared private rather than tagged private, the declaration of Meeting_Type in the visible part would be illegal. The visible part of the child package only has access to the visible part of its parent to ensure that the child cant reveal any private information from its parent in its visible part. As a result, Appointment_Type has to be declared to be a tagged type in the visible part of JE.Appointments so that, in the visible part of the package above, Meeting_Type can be declared to be derived from Appointment_Type. If Appointment_Type wasnt visibly declared to be tagged, the visible declaration of Meeting_Type couldnt extend it.
Notice that Meeting_Type extends Appointment_Type using with private in the visible part of the package. This lets clients of the package know that Meeting_Type is derived from Appointment_Type without providing any information about the extra components it provides. Because of this the compiler will allow clients of the package to use operations inherited from Appointment_Type on Meeting_Type objects. On the other hand, Meeting_Type could simply have been declared to be private:
type Meeting_Type is private;
The disadvantage of doing this would be that clients of the package wouldnt be able to see that Meeting_Type is related to Appointment_Type so that operations inherited from Appointment_Type wouldnt be accessible to its clients (although they would be accessible in the package body, where the full declaration of Meeting_Type is visible).
The functions Date, Details, etc., are inherited from Appointment_Type and can be used unchanged on Meeting_Type objects; there are also two new primitive operations for Meeting_Type called Meeting and Room which allow a meeting to be constructed from its components and the room number to be extracted from a Meeting_Type object. If a further type were to be derived from Meeting_Type, it would inherit all the primitive operations of Meeting_Type; this would mean that it would inherit the primitive operations that Meeting_Type inherited from Appointment_Type (Date, Details, etc.) as well as the new primitive operations Meeting and Room. Room is just an accessor function for the Room component, and Meeting just needs to create an appointment and convert it to a Meeting_Type result with an extension aggregate:
procedure Meeting (Date : in Time_Type; Details : in String; Room : in Room_Type; Result : out Meeting_Type) is A : Appointment; begin Appointment (Date, Details, A); Result := (A with Room => Room); end Meeting;
Meeting_Type also inherits the procedure Put from Appointment_Type. What Put will do is to output a Meeting_Type in exactly the same way as an Appointment_Type; in other words, the extra room number component will be ignored. This is not what we want in this particular case, so we need to override the inherited version of Put with one which can deal with the room numbers as well. This is done by declaring a procedure with exactly the same specification as the inherited procedure (and it must be exact!):
procedure Put (Appt : in Meeting_Type);
The new version of the package now looks like this:
package JE.Appointments.Meetings is subtype Room_Type is Integer range 100 .. 999; type Meeting_Type is new Appointment_Type with private; procedure Meeting (Date : in JE.Times.Time_Type; Details : in String; Room : in Room_Type; Result : out Meeting_Type); function Room (Appt : Meeting_Type) return Room_Type; procedure Put (Appt : in Meeting_Type); -- Date and Details inherited unchanged -- from Appointment_Type private type Meeting_Type is new Appointment_Type with record Room : Room_Type; end record; end JE.Appointments.Meetings;
Note that the rules for child packages mean that if you access JE.Appointments.Meetings in a with clause, you also get access to the parent package JE.Appointments automatically (as well as the ultimate parent package JE, although since this is empty it doesnt give you any extra benefits). This means that given a with clause for the package JE.Appointments.Meetings, you dont need a separate with clause for the package JE.Appointments. However, a use clause for JE.Appointments.Meetings allows you to refer to Meeting_Type directly but it does not let you refer to Appointment_Type directly; youd need a separate use clause for JE.Appointments if you wanted to do this:
with JE.Appointments.Meetings; use JE.Appointments.Meetings, JE.Appointments; procedure X is A : Appointment_Type; -- i.e. JE.Appointments.Appointment_Type M : Meeting_Type; -- i.e. JE.Appointments.Meetings.Meeting_Type begin ... end X;
Alternatively, you could just have a use clause for JE.Appointments and then refer to Meetings.Meeting_Type since the use clause for JE.Appointments lets you refer to JE.Appointments.Meetings simply as Meetings.
The way Ive declared Meeting_Type above is much simpler than the first version at the beginning of the chapter. The version at the beginning contained an Appointment_Type component, which meant that the two types were completely unrelated to each other and that a full set of operations for Meeting_Type had to be defined explicitly, using forwarding to perform the appointment-related work. The new version involving tagged types involves less coding and hence less testing; only the operations that are different and additional operations need to be written and tested, which can reduce the costs of development considerably. Whats more, you can convert between the two types, which means that you can use any procedures youve already written to deal with Appointment_Type objects to deal with Meeting_Type objects; all you have to do is to use a type conversion to convert your Meeting_Type object to an Appointment_Type when you call the procedure.
There is, however, a downside to all this. The derived type does not explicitly list the operations it inherits, so without looking at the parent package (and the grandparent package, and so on) its difficult to know what the complete set of operations for the derived type actually is. You may be very lucky and have access to automated tools which can generate this information automatically from the relevant package specifications (using a class browser), but in general the only reliable solution to this is to provide information about the inherited operations in the documentation, including the use of comments inside the package specification. This requires self-discipline; the compiler wont check what youve written so its up to you to get it right. If you dont provide this sort of information (or if you get it wrong or miss something out) the users of the package will find it much harder to figure out whats going on.
As an example of this, I didnt mention the procedure Appointment which was inherited from Appointment_Type. Without reading the original package specification you wouldnt realise that there was any such procedure available. However, the reason I didnt mention it was that it illustrates yet another danger arising from inheritance in Ada. In many other object-oriented languages certain operations are not inheritable (particularly constructors), but in Ada, all primitive operations are inherited. The specification of the procedure that Meeting_Type inherits will look like this:
procedure Appointment (Date : in Time_Type; Details : in String; Result : out Meeting_Type);
The result of calling this procedure will be that the Meeting_Type result will be created in exactly the same way as it was when the result was an Appointment_Type; the room number wont have been set up. The procedure Meeting doesnt override this because the name is different, and even if Meeting was renamed Appointment it wouldnt override it since the parameter lists are different (Meeting has an extra parameter for the room number) so that there would just be two procedures called Appointment. This is a serious problem because it provides a way to construct Meeting_Type objects incorrectly. One solution is to override Appointment so that it provides a default value for the room number; another possibility is to raise an exception if Appointment is called to create a meeting:
procedure Appointment (Date : in Time_Type; Details : in String; Result : out Meeting_Type) is begin -- This procedure should never be called raise Program_Error; end Appointment;
This is unsatisfactory because it involves declaring an unnecessary procedure which should never be called; it is also unsatisfactory because if Appointment is accidentally called to initialise a meeting, detection of the error will happen at run time instead of at compile time.
In the original package Appointment was declared as a function:
function Appointment (Date : Time_Type; Details : String) return Appointment_Type;
If a function is a primitive operation of a tagged type and it returns a result of that type, any derived types inherit what is called an abstract operation, in other words a function for which no implementation exists. Since Meeting_Type is an extension of Appointment_Type, its no good just returning an Appointment_Type result and pretending its a Meeting_Type since the room number wont have been set up. The compiler will insist that you explicitly override any inherited abstract operations:
function Appointment (Date : Time_Type; Details : String) return Meeting_Type;
You might be able to set the room number to some sort of default value, but the most sensible definition you could provide for this function would be one that raises an exception as described above since youll probably need a similar function for meetings anyway:
function Meeting (Date : Time_Type; Details : String; Room : Room_Type) return Meeting_Type;
At least if you use a function the compiler will tell you that youve got a problem; when you have a problem like this involving a procedure its up to you to realise that youve got a problem and to do something about it. For this reason you should be very careful whenever youve got a type with a primitive procedure which has an out or in out parameter of the type in question.
Another solution is to declare Appointment in a separate package so that it isnt a primitive operation of Appointment_Type and so it wont be inherited by Meeting_Type. One way to do this is to use another package inside Appointments:
package JE.Appointments is type Appointment_Type is tagged private; ... -- as before -- Internal package containing constructor function: package Create is function Appointment (Date : Time_Type; Details : String) return Appointment_Type; end Create; private ... -- as before end JE.Appointments;
Now since Appointment is declared in a different package it wont be a primitive operation of Appointment_Type. The inner package body will need to be defined within the body of the outer package, like this:
package body JE.Appointments is ... -- bodies of primitive subprograms go here package body Create is function Appointment (Date : Time_Type; Details : String) return Appointment_Type is ... end Appointment; end Create; end JE.Appointments;
The constructor can now be called using the name JE.Appointments.Create.Appointment, or if youve chosen to provide a use clause for JE.Appointments you can refer to it more simply as Create.Appointment.
Another way to do the same thing is to define the constructor as a child of the package JE.Appointments:
function JE.Appointments.Create (Date : Time_Type; Details : String) return Appointment_Type is ... end JE.Appointments.Create;
The constructor is now called JE.Appointments.Create, which is slightly simpler to remember.
Of course, there are other cases where you want to override an existing primitive operation but you make a silly mistake:
procedure Appointment (Details : in String; Date : in Time_Type; Result : out Meeting_Type);
The parameters are now in a different order, so the procedures signature is different. The compiler will interpret this as overloading the procedure name with a separate meaning rather than overriding the existing procedure with a new meaning, and youll end up with two procedures called Appointment. To be safe, you should copy the parent specification and then edit it (carefully!) so that you dont make mistakes like this when youre defining your derived type.
The final verdict? Beware! Inheritance is powerful but potentially dangerous. There are lots of subtle traps that the compiler cant detect. You have to be very careful what you do. All the same, you have to risk it; the alternative is to end up trapped in a non-extensible cul-de-sac when its maintenance time.
I started off this chapter by using containment to simulate inheritance, where the derived type contained an object of the parent type. This is always a possibility, but inheritance can sometimes make things easier. So how can you tell when to use inheritance rather than containment (or vice versa) to establish relationships between things?
Inheritance is often characterised as an is-a or an is-kind-of relationship between two types. By deriving type Derived from type Parent, what youre saying is that a Derived is a kind of Parent. For example, it would make sense to derive a type Car from another type Vehicle, since a car is a kind of vehicle. However, it would not make sense to derive type Engine from type Car since an engine is not a type of car; it is instead a component of a car. The relevant declarations might look something like this in Ada:
type Vehicle is tagged record ... -- properties common to all Vehicles end record; type Car is new Vehicle with -- Car is inherited from Vehicle record E : Engine; -- Engine is a component of Car ... end record;
The relationship between an engine and a car is a has-a or an is-part-of relationship (a car has an engine; an engine is part of a car) rather than an is-kind-of relationship, or in other words a containment relationship (since a car contains an engine). Identifying the correct relationship is very important; youve already seen how complicated things can get if containment is used instead of inheritance at the beginning of this chapter. Similarly, inappropriate use of inheritance can cause problems. Imagine that Car had a Headlight component and an operation called Flash_Headlights, and that we derived Engine from Car by mistake. Engine would inherit Headlight and Flash_Headlights from Car. I dont know about you, but Ive never come across an engine with headlights, still less one that could flash them! Whats more, you could convert an engine into a car whenever you wanted to; this would not go down well with the licensing authorities.
What about a Driver? Its obviously not a kind of Car, so there isnt an inheritance relationship between Car and Driver; neither is a driver a component of a car, since a car can exist without a driver or can have different drivers at different times; similarly a driver might drive different cars at different times. This shows that theres another possible relationship between types: an association which can be characterised as an is-used-by or an is-associated-with relationship.
In the case of vehicles, cars, engines and drivers the relationships are fairly obvious, but there are other cases that are much less straightforward. For example, consider a point in a two-dimensional plane which is represented by its X and Y coordinates. If you want to generalise this to a three-dimensional point represented by three coordinates (X, Y and Z) you could use either containment (a 3D point contains a 2D point together with a Z coordinate) or inheritance (a 3D point is a kind of 2D point with an extra Z coordinate). The main difference is that if inheritance is used it becomes possible to convert a 3D point to a 2D point by discarding the Z coordinate. Depending on the sort of primitive operations defined for 2D points, you might inherit inappropriate operations if a 3D point were derived from a 2D point. For this reason I would probably prefer to use containment since it is likely that you would want to choose which two-dimensional plane you project a three-dimensional point on to; there might also be considerations such as the need for perspective transformations.
Another interesting case is the relationship between squares and rectangles. A square is a kind of rectangle, or is it the other way round? A square has a width (which is also its length), but a rectangle might be considered to be an extension of this which has an extra length component independently of its width:
type Square is tagged record Width : Float; end record; type Rectangle is new Square with record Length : Float; end record;
However, most people would prefer to say that a square is a special case of a rectangle rather than saying that a rectangle is a generalisation of a square. Inheritance doesnt let us remove existing features, so this is something we cant represent using inheritance. A good way to choose between containment and inheritance is to ask yourself if you would want to be able to convert a rectangle to a square (or vice versa). In difficult cases like this where the two types are obviously related in some way (e.g. for use in a drawing program which can draw squares, rectangles and other shapes), but where the relationship isnt at all obvious, you can usually break the Gordian knot by deriving them both from some other parent type Shape:
type Shape is tagged record ... -- properties common to all shapes end record; type Square is new Shape with record Width : Float; end record; type Rectangle is new Shape with record Width : Float; Length : Float; end record;
This illustrates a useful design principle: difficult decisions about the relationship between two types can quite often be resolved by introducing a third type which is used to encapsulate the common features of the other two types.
14.1 | Modify the linked list package JE.Lists to define List_Iterator as a tagged type. Derive a new Sorted_List_Iterator with an overridden Insert operation which always inserts items into the list in the correct position so that the items in the list are always in ascending order. You will need to supply a suitable comparison operation as a generic parameter. Test that it works by modifying the diary program to use it for inserting appointments into the diary. |
14.2 | Use a variant record to define a shape which can be a circle (with a radius) or a square (with a width) or a rectangle (with a width and length). Define a function Area to return the area of a given shape. Write a test program to create some shapes and display their areas. |
14.3 | Define a tagged type to represent a bank account with operations to deposit and withdraw money and to query the balance of the account. Derive another bank account type which allows the account to be overdrawn up to a defined limit and another one which charges a fixed fee for every withdrawal. Write a test program to test the different account types. |
14.4 | Define a new appointment type which records the duration of an appointment in minutes, and modify the diary program so that it asks for the duration of new appointments and so that it displays the start and end times of each appointment (e.g. 10:00 10:30 for a 30 minute appointment at 10:00). |
Previous |
Contents |
Next |
This file is part of
Ada 95: The Craft of Object-Oriented Programming
by John English.
Copyright © John
English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational
use only, provided that all the constituent files are distributed without
change.