Previous |
Contents |
Next |
We explain the behaviour of a component
at any given level
in terms of interactions between subcomponents whose own
internal organization, for the moment, is taken for granted.
Richard Dawkins, The Blind Watchmaker
Having looked at how private types can be used to implement abstract data types (ADTs) in Ada, lets return to the electronic diary program of chapter 8 and try to redesign it using abstract data types to avoid the sorts of maintenance problems highlighted at the beginning of chapter 9. Rather than starting by trying to break down the program into smaller subproblems as in the top-down approach, the starting point with a design based around abstract data types is to decide what types of objects in the real world the program is going to need to represent. This is the basis of an object-oriented approach to the design. In the case of an electronic appointments diary the answer is fairly obvious; the program needs to model a diary, so well need an ADT called Diary_Type. The diary contains a collection of appointments, so well need another ADT which Ill call Appointment_Type. A good rule of thumb is to look at the program specification; the nouns it uses (diary, appointment, and so on) describe the objects it deals with and are therefore possible candidates for ADTs, although some (e.g. user) will be red herrings. By examining the nouns used in the specification you can draw up a list of potential ADTs and then eliminate synonyms and red herrings. At this point the types can just be described as private so that their internal workings dont have to be considered until later. Each ADT should go in a separate package unless theres a very good reason to the contrary (e.g. they need to be able to see each others full declaration for some reason).
The next step is to identify the operations that each ADT supports. Again, a useful rule of thumb is to look at the verbs in the specification which are used in connection with the corresponding nouns: add an appointment to the diary, list the appointments in the diary, and so on. These describe the actions to be performed by the objects. From this you can build up a list of the operations that each ADT must provide and write subprogram specifications to match. By now you should have an outline of the visible part of the package specification for each of the ADTs and you can concentrate on writing the program in terms of these ADTs and the operations they provide.
This is no longer a top-down approach; the top-down approach only works if you know where the bottom is that your design is heading towards. By sketching out the design of the ADTs in advance you are providing yourself with a known bottom layer to aim for, and you can then steer your top-down design towards it. In other words, you combine a top-down approach with a bottom-up approach with the aim of getting the two to meet in the middle. The nitty-gritty implementation details of each ADT operation can then be the subject of another iteration of the same design process, so that each ADT is implemented in terms of further ADTs. The result will be an object-oriented design; each type of object (i.e. each ADT) provides a set of services that other objects can use without revealing how those services are implemented, and in turn uses the services provided by other ADTs without needing to know how they are implemented. At the bottom level the ADTs are those provided for in the language specification: Integer, Float, Boolean, String, Ada.Exceptions.Exception_Id, Ada.Calendar.Time and so on. As long as the set of services that an ADT provides is sufficient for the needs of its clients and independent of any particular implementation you should be able to reimplement any of the ADTs without affecting the overall design of the system. Defining the interaction of the ADTs is the most important part of the design process; implementing them is a secondary concern.
One of the most important aspects in a successful design is separating the modelling of the aspects of the real world that the program is concerned with (e.g. the functions of a diary) from the user interface which allows a user to interact with it. The user interface should always be the outermost layer of a program; its the part of the program thats most likely to change and nothing else in the program should depend on it. At the moment the diary has a simple textual interface, but this might need to be a graphical interface in another program or another version of the same program. The program uses a model (an abstract data type of some sort) to represent something in the real world. It also provides a user interface, a view of the data represented in the model which gives the users a way of interacting with (viewing and controlling) that model. Different views of the same model might be required; you might want to be able to view a table of numbers as a pie chart or some sort of graph. The way that you interact with the program is likely to depend on how youre viewing the data; if its a graph, you might want to alter it by dragging points on the graph around the screen rather than by typing in numbers. You might also want multiple different views of the same data at the same time. In the case of a diary you might want to be able to look at the appointments or you might want a day-planner view which shows the times you are free and the times you are busy, or you might want both views at once.
With this approach, the program is responsible for tying together the model and the view. The model itself should be completely independent of the program; this can be achieved by defining the model as an abstract data type in a package. The view will of course be heavily dependent on the model and will be tailored to the needs of the particular program, but the program shouldnt have any special knowledge of the internal details of the views implementation so that these can be changed if necessary. One way to manage this would be to define a set of user interface procedures which could be compiled separately from the main program, but a better way is to define the view in yet another package. This will give us the freedom to hide additional implementation details in the package body (data types, procedures, etc.) which are specific to the view and which the main program should not need to see.
Since views are program specific, the package defining the view might actually be defined inside the main program. Heres the sort of thing you might do in the diary program:
with JE.Diaries; procedure Diary is package Diary_View is type Command_Type is (Add, List, Delete, Save, Quit); function Next_Command return Command_Type; procedure Load_Diary (Diary : in out JE.Diaries.Diary_Type); procedure Save_Diary (Diary : in JE.Diaries.Diary_Type); procedure Add_Appointment (Diary : in out JE.Diaries.Diary_Type); procedure List_Appointment (Diary : in JE.Diaries.Diary_Type); procedure Delete_Appointment (Diary : in out JE.Diaries.Diary_Type); end Diary_View; package body Diary_View is separate; ... -- etc. begin ... -- body of program end Diary;
The subprograms declared in Diary_View will all be concerned with the user interface, interacting with the user to get the details of an appointment and then using the facilities of JE.Diaries to add the appointment to the diary.
Notice that a package can be defined inside a procedure rather than being made into a separate library unit. Both the specification and the body must be declared at the same level, so for library packages they are both library units. Inside a procedure they must be declared in the same declaration section.
The package specification tells us absolutely nothing about the appearance of the user interface; it just provides a list of possible commands, a function for getting the next command, and a set of procedures to interact with the user in order to implement those commands. The commands themselves are completely abstract; they may be characters typed at a keyboard or items selected from a pull-down menu. All the program ever sees is values of type Command_Type.
The package body is defined as separate, so somewhere else we need to define it like this:
separate (Diary) package body Diary_View is ... end Diary_View;
The great advantage of this approach is that the package body can provide any internal data structures or subprograms that it needs as well as its own initialisation code (e.g. creating a window on the screen) without the main program having to know anything about it at all. I described in chapter 4 how a package initialisation section could be used to display copyright notices when a program starts up; the same facility can be used to perform any other package-specific initialisation (although you have to be careful to handle every exception that might occur, since any unhandled exceptions will abort the program before the main procedure gets started!). The separate package body can also have its own with clauses to allow it to reference any external packages it needs. If the user interface changes, only the procedures in the package body need to be changed; if multiple views of the diary are needed, List_Appointments can display whatever views the user selects and Next_Command can respond to the users interactions with any of those views without the program having to be involved in view management. All the program ends up doing is providing the model and using the package Diary_View to get and respond to commands from the user:
with JE.Diaries; procedure Diary is package Diary_View is type Command_Type is (Add, List, Delete, Save, Quit); function Next_Command return Command_Type; procedure Load_Diary (Diary : in out JE.Diaries.Diary_Type); procedure Save_Diary (Diary : in JE.Diaries.Diary_Type); procedure Add_Appointment (Diary : in out JE.Diaries.Diary_Type); procedure List_Appointments (Diary : in JE.Diaries.Diary_Type); procedure Delete_Appointment (Diary : in out JE.Diaries.Diary_Type); end Diary_View; package body Diary_View is separate; Diary_Model : JE.Diaries.Diary_Type; begin begin Diary_View.Load_Diary (Diary_Model); exception when JE.Diaries.Diary_Error => null; -- ignore errors when trying to load the diary end; loop case Diary_View.Next_Command is when Diary_View.Add => Diary_View.Add_Appointment (Diary_Model); when Diary_View.List => Diary_View.List_Appointments (Diary_Model); when Diary_View.Delete => Diary_View.Delete_Appointment (Diary_Model); when Diary_View.Save => Diary_View.Save_Diary (Diary_Model); when Diary_View.Quit => exit; end case; end loop; end Diary;
The program tries to load the diary, and then processes commands one after another. Ive assumed an exception called Diary_Error will be reported if anything goes wrong during loading so that errors encountered when loading the diary can be ignored by the exception handler. Each command is directed to the appropriate operation in Diary_View except for Quit, which just breaks out of the main loop to end the program.
Now its time to consider the design of the types used to represent the diary and its appointments. We know we need to represent a diary, so well need a type which Ive called Diary_Type. Diary_Type will need to be declared in a package which Ive called JE.Diaries. Should Diary_Type be visible? No, because we might want to change the implementation at a later date. It needs to be a private type or a limited private type. In general only scalar types like Day_Type and Month_Type should be made visible; they are usually needed as their values are the building blocks used to construct composite values like dates. Do we want to be able to assign one diary to another? Probably not, since this would allow an existing diary to be overwritten (but merging the appointments from one diary with the appointments in another might be a sensible operation to provide). If we dont want to allow assignment, the diary should be declared limited private. The diary will be a collection of appointments, so well need to define another type Appointment_Type for the individual appointments. This can go in another package called JE.Appointments. We dont want users to be able to see how weve implemented these, so the type should be private, but we probably do want to be able to copy appointments so it wont need to be a limited type.
Next we need to identify the operations that a diary should provide. We can do this by looking at the specification for the original problem:
It will need to provide as a minimum the ability to add new appointments, delete existing appointments, display the list of appointments and save the appointments to a file. Also, if there are any saved appointments we should read them in when the program starts up.
(Most specifications that youll be given will hopefully be more detailed than this!) The operations are similar to those from chapter 8; we need to be able to add appointments, delete specified appointments, extract individual appointments in order to list them, save the diary to a file and load it from a file.
Heres a package specification based on these initial thoughts:
with JE.Appointments; use JE.Appointments; package JE.Diaries is type Diary_Type is limited private; procedure Load (Diary : in out Diary_Type; From : in String); procedure Save (Diary : in Diary_Type; To : in String); procedure Add (Diary : in out Diary_Type; Appt : in Appointment_Type); procedure Delete (Diary : in out Diary_Type; Appt : in Positive); function Choose (Diary : Diary_Type; Appt : Positive) return Appointment_Type; Diary_Error : exception; private ... -- it's a secret! end JE.Diaries;
Load and Save both take a diary and a string as their parameters; the diary will be loaded from or saved to the file named by the Name parameter. Add takes a diary and an appointment as its parameters and adds the appointment to the diary. Delete takes a diary and an appointment number as its parameters and deletes the specified appointment; Choose also takes a diary and an appointment number as its parameters and returns a copy of the selected appointment. Earlier I assumed the existence of an exception called Diary_Error which would be reported if anything went wrong when loading the diary, which is declared here. It can also be used for reporting errors arising from other operations. In the case of Add, the diary might be full, and in the case of Delete and Choose the appointment number might be out of range, so Diary_Error can be used to report these errors. An extra function to return the number of appointments in the diary would also be useful, since this will allow clients to test if an appointment number is valid before calling Delete or Choose:
function Size (Diary : Diary_Type) return Natural;
The chances are that youll overlook a few minor details like this in the early stages of any design and then discover the need for them as you get more involved in the implementation. This is normal; dont worry about it. As you get more and more experienced youll start spotting these things earlier, but in the meantime theres nothing wrong with having to go back and make a few minor changes occasionally.
Appointment_Type needs to be dealt with next. An appointment consists of a date (day, month, year, hour and minute) and a description; we will need to be able to extract these via accessor functions and construct an appointment from its components with a constructor function so that values can be transferred to and from the user interface. The package JE.Times from the previous chapter provides Time_Type which can be used for the date, and the description will just be a String.
Heres an outline for JE.Appointments which shows the specifications for the accessor and constructor functions that Appointment_Type requires:
with JE.Times; use JE.Times; package JE.Appointments is type Appointment_Type is private; -- Accessor functions function Date (Appt : Appointment_Type) return Time_Type; function Details (Appt : Appointment_Type) return String; -- Constructor function Appointment (Date : Time_Type; Details : String) return Appointment_Type; private ... -- it's a secret! end JE.Appointments;
Given the design described above for JE.Diaries and JE.Appointments, we can turn to considering the body of the internal package Diary_View. This version will be a textual interface based on Ada.Text_IO. In outline it will look like this:
with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; separate (Diary) package body Diary_View is function Next_Command return Command_Type is ... end Next_Command; procedure Load_Diary (Diary : in out JE.Diaries.Diary_Type) is ... end Load_Diary; procedure Save_Diary (Diary : in JE.Diaries.Diary_Type) is ... end Save_Diary; procedure Add_Appointment (Diary : in out JE.Diaries.Diary_Type) is ... end Add_Appointment; procedure List_Appointments (Diary : in JE.Diaries.Diary_Type) is ... end List_Appointment; procedure Delete_Appointment (Diary : in out JE.Diaries.Diary_Type) is ... end Delete_Appointment; end Diary_View;
Lets consider each of these in turn. Next_Command will be responsible for displaying a menu and getting the users response. Most of this code can be taken from the program in chapter 8:
function Next_Command return Command_Type is Command : Character; begin loop -- display menu New_Line (5); Put_Line ("Diary menu:"); Put_Line (" [A]dd appointment"); Put_Line (" [D]elete appointment"); Put_Line (" [L]ist appointments"); Put_Line (" [S]ave appointments"); Put_Line (" [Q]uit"); New_Line; Put ("Enter your choice: "); -- get a key Get (Command); Skip_Line; -- return selected command case Command is when 'A' | 'a' => return Add; when 'D' | d' => return Delete; when 'L' | 'l' => return List; when 'S' | 's' => return Save; when 'Q' | 'q' => return Quit; when others => Put_Line ("Invalid choice -- " & "please enter A, D, L, S or Q"); end case; end loop; exception when End_Error => -- quit if end-of-file character entered return Quit; end Next_Command;
The function displays the menu, gets the users choice and then returns the appropriate Command_Type value for the choice. If the user enters an incorrect character an error message is displayed before looping back to display the menu again. End-of-file errors are handled by treating them as Quit commands.
Listing the appointments is also done in much the same way as before, except that the procedure Choose must be used to get the appointment; since Diary_Type is private, we cant just access it as an array:
procedure List_Appointments (Diary : in JE.Diaries.Diary_Type) is Appt : JE.Appointments.Appointment; begin if JE.Diaries.Size(Diary) = 0 then Put_Line ("No appointments found."); else for I in 1 .. JE.Diaries.Size(Diary) loop Put (I, Width=>3); Put (") "); Put (JE.Diaries.Choose(Diary,I)); New_Line; end loop; end if; end List_Appointments;
A version of Put for Appointment_Type values will be needed within Diary_View. This will be a private operation hidden inside the package body; the implementation of it could be based on the version which was given in chapter 8.
Add_Appointment just needs to get the date, time and details of an appointment from the user and then use the Add procedure from JE.Diaries to add the new appointment to the diary:
procedure Add_Appointment (Diary : in out JE.Diaries.Diary_Type) is package Month_IO is new Ada.Text_IO.Enumeration_IO (JE.Times.Month_Type); use Month_IO; Day : JE.Times.Day_Type; Month : JE.Times.Month_Type; Year : JE.Times.Year_Type; Hour : JE.Times.Hour_Type; Minute : JE.Times.Minute_Type; Details : String (1..50); Length : Natural; Separator : Character; begin Put ("Enter date: "); Get (Day); Get (Separator); Get (Month); Get (Separator); Get (Year); Skip_Line; Put ("Enter time: "); Get (Hour); Get (Separator); Get (Minute); Skip_Line; Put ("Description: "); Get_Line (Details, Length); JE.Diaries.Add ( Diary, JE.Appointments.Appointment ( JE.Times.Time (Day, Month, Year, Hour, Minute), Details(1..Length) ) ); exception when Data_Error | Constraint_Error | JE.Times.Time_Error => Put_Line ("Invalid input."); end Add_Appointment;
A single separator character is read between each component of the date and time; this allows the user to enter any separator rather than requiring the components to be separated by spaces (e.g. 25-Dec-1995 for a date or 10.15 for a time).
The appointment details will be read into a string which is defined arbitrarily as being 50 characters long; this is done without reference to the diary package which just uses String as the type for the appointment details without revealing what the maximum length it allows is. The length of the string that the user is allowed to type in is then a property of the user interface rather than the diary and the maximum length that an appointment can hold is a property of the diary package; the two are kept independent of each other.
Deleting an appointment involves asking the user to enter an appointment number, checking that its valid and then calling JE.Diaries.Delete to handle the actual deletion:
procedure Delete_Appointment (Diary : in out JE.Diaries.Diary_Type) is Appt_No : Positive; begin Put ("Enter appointment number: "); Get (Appt_No); if Appt_No not in 1 .. JE.Diaries.Size(Diary) then raise Constraint_Error; end if; JE.Diaries.Delete (Diary, Appt_No); exception when Constraint_Error | Data_Error => Put_Line ("Invalid appointment number"); Skip_Line; end Delete_Appointment;
Finally, here are Load_Diary and Save_Diary. These just call JE.Diaries.Load to load the diary and JE.Diaries.Save to save the diary:
procedure Load_Diary (Diary : in out JE.Diaries.Diary_Type) is begin JE.Diaries.Load (Diary, Diary_Name); end Load_Diary; procedure Save_Diary (Diary : in JE.Diaries.Diary_Type) is begin JE.Diaries.Save (Diary, Diary_Name); end Save_Diary;
The diary name can be defined as a constant inside the package body:
Diary_Name : constant String := "Diary";
An alternative implementation might search a predefined list of places to find the file or might ask the user for the filename; it might also allow multiple files to be loaded and merged.
So far we havent needed to consider how the diary and appointment packages are actually implemented. One of the interesting things about an object-oriented approach to design is that, once the behaviour of the objects has been defined, writing the code to provide that behaviour can be treated as mere detail. It still needs to be done, though! First well need to define the actual representations of the private types:
with JE.Times; use JE.Times; package JE.Appointments is type Appointment_Type is private; ... -- etc. private type Appointment_Type is record Time : Time_Type; Details : String (1..50); -- an arbitrary size Length : Natural := 0; end record; end JE.Diaries; with JE.Appointments; use JE.Appointments; package JE.Diaries is type Diary_Type is limited private; ... -- etc. private type Appointment_Array is array (Positive range <>) of Appointment_Type; type Diary_Type is limited record Appts : Appointment_Array (1..10); -- an arbitrary size Count : Natural := 0; end record; end JE.Diaries;
These declarations are the same as the ones given in chapter 8, except that the date and time are represented using JE.Times.Time_Type and that Diary_Type doesnt use a discriminant for the number of appointments any more. The package body for JE.Appointments will look like this in outline:
package body JE.Appointments is function Date (Appt : Appointment_Type) return Time_Type is ... end Date; function Details (Appt : Appointment_Type) return String is ... end Details; function Appointment (Date : Time_Type; Details : String) return Appointment_Type is ... end Appointment; end JE.Appointments;
The package body for JE.Diaries needs to provide bodies for the subprograms declared in the package specification, so in outline it will look like this:
package body JE.Diaries is function Size (Diary : Diary_Type) return Natural is ... end Size; procedure Load (Diary : in out Diary_Type; From : in String) is ... end Load; procedure Save (Diary : in Diary_Type; To : in String) is ... end Save; procedure Add (Diary : in out Diary_Type; Appt : in Appointment_Type) is ... end Add; procedure Delete (Diary : in out Diary_Type; Appt : in Positive) is ... end Delete; function Choose (Diary : Diary_Type; Appt : Positive) return Appointment_Type is ... end Choose; end JE.Diaries;
These outlines are taken directly from the package specifications. All that remains is to implement the bodies of the subprograms in each package. Ill deal with the appointment package first. The appointment accessors are very straightforward; they can be implemented like this:
function Date (Appt : Appointment_Type) return Time_Type is begin return Appt.Time; end Date; function Details (Appt : Appointment_Type) return String is begin return Appt.Details (1..Appt.Length); end Details;
The constructor for appointments is marginally more complex since it needs to take into account the fact that the Details parameter might be longer than the appointment can hold, in which case only the first part of the string should be copied into the appointment:
function Appointment (Date : Time_Type; Details : String) return Appointment_Type is A : Appointment_Type; begin A.Time := Date; if Details'Length > A.Details'Length then A.Details := Details(Details'First .. Details'First+A.Details'Length-1); A.Length := A.Details'Length; else A.Details (1 .. Details'Length) := Details; A.Length := Details'Length; end if; return A; end Appointment;
Now for the operations on Diary_Type objects. Size is the simplest of these; its just an accessor function for the Count component of a diary:
function Size (Diary : Diary_Type) return Natural is begin return Diary.Count; end Size;
Choose is likewise an accessor for a specific appointment within the array of appointments in a diary. It needs to check that the appointment number is valid and raise a Diary_Error exception if it isnt:
function Choose (Diary : Diary_Type; Appt : Positive) return Appointment_Type is begin if Appt not in 1 .. Diary.Count then raise Diary_Error; else return Diary.Appts(Appt); end if; end Choose;
Deleting an appointment involves checking that the appointment number is valid and then moving appointments up the array to overwrite the appointment being deleted:
procedure Delete (Diary : in out Diary_Type; Appt : in Positive) is begin if Appt not in 1 .. Diary.Count then raise Diary_Error; else Diary.Appts(Appt..Diary.Count-1) := Diary.Appts(Appt+1..Diary.Count); Diary.Count := Diary.Count - 1; end if; end Delete;
Adding an appointment involves locating the correct place in the array for the new appointment, moving appointments down the array to make room for it and then inserting the new appointment into the vacated array element. Diary_Error will need to be raised if the diary is full:
procedure Add (Diary : in out Diary_Type; Appt : in Appointment_Type) is use type JE.Times.Time_Type; -- to allow use of ">" Pos : Positive; -- position for insertion begin if Diary.Count = Diary.Appts'Length then raise Diary_Error; else Pos := 1; for I in 1 .. Diary.Count loop exit when Date(Diary.Appts(I)) > Date(Appt); Pos := Pos + 1; end loop; Diary.Appts(Pos+1..Diary.Count+1) := Diary.Appts(Pos..Diary.Count); Diary.Appts(Pos) := Appt; Diary.Count := Diary.Count + 1; end if; end Add;
A use type clause is needed to allow the operator ">" to be accessed directly. The package body will of course need a with clause for JE.Times.
Rather than saving the diary as a text file (which would involve unpicking each appointment into its component parts) the appointments can be saved in their internal form using the package Ada.Sequential_IO. This is a generic package that needs to be instantiated for the type of data to be stored in the file; the full specification is given in Appendix B. It provides essentially the same facilities as Ada.Text_IO except that the input and output procedures are called Read and Write instead of Get and Put. The package body will need a with clause for Ada.Sequential_IO and an instantiation for Appointment_Type:
with Ada.Sequential_IO, JE.Times; package body JE.Diaries is package Appt_IO is new Ada.Sequential_IO (Appointment_Type); ... end JE.Diaries;
Save needs to try to create the output file and then to write each appointment in turn into the file. Diary.Count cant be written to the file any more since only Appointment_Type values can be written:
procedure Save (Diary : in Diary_Type; To : in String) is File : Appt_IO.File_Type; begin Appt_IO.Create (File, Name => To); for I in 1..Diary.Count loop Appt_IO.Write (File, Diary.Appts(I)); end loop; Appt_IO.Close (File); end Save;
Load essentially reverses the process; there is no appointment count in the file now, so it needs to check for End_Of_File to discover when its finished reading the file. Heres how Load can be implemented:
procedure Load (Diary : in out Diary_Type; From : in String) is File : Appt_IO.File_Type; begin Diary.Count := 0; Appt_IO.Open (File, Name => From, Mode => Appt_IO.In_File); while not Appt_IO.End_Of_File(File) loop Diary.Count := Diary.Count + 1; Appt_IO.Read (File, Diary.Appts(Diary.Count)); end loop; Appt_IO.Close (File); exception when Appt_IO.Name_Error => raise Diary_Error; end Load;
The diary size (Diary.Count) is set to zero at the very beginning of the procedure so that its guaranteed to be valid if an exception occurs. The exception handler will handle Name_Errors by raising a Diary_Error exception so that the main program can decide how to deal with it, in accordance with the principle that package operations shouldnt do their own error handling.
So, how much better is this design than the one in chapter 8? We can assess this by considering the maintenance scenarios described at the beginning of chapter 9: having multiple diaries, using a graphical user interface, and integrating the diary into an electronic mail system. The last of these will now be easy to do; JE.Diaries is completely independent of the program in this chapter so it could be used unchanged in any other application that needed it. Multiple diaries could be handled by declaring an array of Diary_Types in the program and providing extra commands to open and close diaries as well as selecting a particular diary as the current diary to be used when adding or deleting appointments. The appropriate array element could then be passed as the parameter to Add_Appointment, Delete_Appointment and so on.
The way that the model has been separated from the view will make it fairly easy to revise for use with a graphical user interface. In a graphical environment the commands might be selected from pull-down menus; Next_Command will just need to return command codes whenever one of the diary handling commands is selected. Extra commands might be needed to manage aspects of the user interface, e.g. commands to control the placement and size of windows. These commands wouldnt affect the model, so they could be handled internally within the Diary_View package. The List command might be redundant since in a graphical environment the appointments would probably be visible in a window at all times. This is no problem; if this were the case, the interfaces menu wouldnt provide a List command and Next_Command would never return List as its result. Add_Appointment would be quite easy to rewrite since it does all the necessary interaction with the user to get the appointment details; youd just need to replace the procedure with a version which used a graphical dialog to get the details instead. When deleting appointments, the appointment to be deleted might be selected by pointing at one of the appointments displayed on the screen. It would still be possible for the user interface code to work out what the corresponding appointment number was by keeping track of which appointments were visible, so the fact that the Diary_Type abstraction identifies appointments by number shouldnt be a problem. Also, the user might be able to select multiple appointments for deletion; Delete_Appointment would then need to incorporate a loop to get the numbers corresponding to the selected appointments and delete them one by one.
The program now exhibits the object-oriented structure that I described at the beginning of the chapter. The program defines the user interface and uses the services provided by the other ADTs in the design (the diary and the appointments). Appointments rely on an ADT which provides date and time services (JE.Times), and so on. Individual ADTs can be changed independently as long as the set of services needed by their clients is still available. However, we havent eliminated maintenance problems completely. Maintenance requirements like the ones described will still involve a fair amount of work, but the way that the different aspects of the program have been compartmentalised will make maintenance much easier than it was before. Also, as well see in later chapters, there are other maintenance issues which the current design will still have difficulties coping with.
10.1 | Modify the diary program in this chapter to allow the user to specify the name of the diary file to be used. |
10.2 | Modify the diary program to allow the user to open multiple diaries at the same time and switch from using one diary to another. |
10.3 | Once the ability to open multiple diaries has been provided, add the ability to copy or move appointments from one diary to another. |
10.4 | Once the ability to open multiple diaries has been provided, add a command which allows the user to display a merged list of all the appointments in all the diaries that are currently open. The appointments should still be listed in order of date and time. |
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.