Previous

Contents

Next

Chapter 10:
Designing with abstract data types

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


10.1 The design process revisited
10.2 Separating out the user interface
10.3 Designing the model
10.4 Defining the view package
10.5 Implementing the ADT packages
10.6 Diary operations
10.7 Maintenance issues
Exercises

10.1 The design process revisited

Having looked at how private types can be used to implement abstract data types (ADTs) in Ada, let’s 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 we’ll need an ADT called Diary_Type. The diary contains a collection of appointments, so we’ll need another ADT which I’ll 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 don’t have to be considered until later. Each ADT should go in a separate package unless there’s a very good reason to the contrary (e.g. they need to be able to see each other’s 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.


10.2 Separating out the user interface

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; it’s the part of the program that’s 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 you’re viewing the data; if it’s 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 shouldn’t have any special knowledge of the internal details of the view’s 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. Here’s 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 user’s 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. I’ve 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.


10.3 Designing the model

Now it’s 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 we’ll need a type which I’ve called Diary_Type. Diary_Type will need to be declared in a package which I’ve 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 don’t want to allow assignment, the diary should be declared limited private. The diary will be a collection of appointments, so we’ll need to define another type Appointment_Type for the individual appointments. This can go in another package called JE.Appointments. We don’t want users to be able to see how we’ve implemented these, so the type should be private, but we probably do want to be able to copy appointments so it won’t 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 you’ll 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.

Here’s 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 you’ll 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; don’t worry about it. As you get more and more experienced you’ll start spotting these things earlier, but in the meantime there’s 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.

Here’s 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;

10.4 Defining the view package

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;

Let’s consider each of these in turn. Next_Command will be responsible for displaying a menu and getting the user’s 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 user’s 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 can’t 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 it’s 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.


10.5 Implementing the ADT packages

So far we haven’t 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 we’ll 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 doesn’t 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. I’ll 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;

10.6 Diary operations

Now for the operations on Diary_Type objects. Size is the simplest of these; it’s 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 isn’t:

    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 can’t 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 it’s finished reading the file. Here’s 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 it’s 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 shouldn’t do their own error handling.


10.7 Maintenance issues

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 wouldn’t 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 interface’s menu wouldn’t 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; you’d 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 shouldn’t 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 haven’t 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 we’ll see in later chapters, there are other maintenance issues which the current design will still have difficulties coping with.


Exercises

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.