The term abstract data type is about as dry and technical sounding as you can get. Yet the concept of an abstract data type, or ADT, is something we apply -- or should apply -- in every single one of our application efforts, sometimes without even realizing that we are doing it. An abstract data type is a collection of information and operations that act on that information. An ADT can represent computer objects such as lists, records, stacks, arrays, and tables; but an ADT can also represent real-world objects, such as a company, a product, or the set of assembly line operations at a factory.
The power behind the ADT lies in the first word of its name: abstract. When you create an ADT, you work with objects as opposed to variables, columns, and other computer science items. You perform an abstraction from the implementation details to the "thing in itself" and work on a higher level.
PL/SQL offers several different constructs with which to build and manage ADTs, most importantly the package. The most general description of an ADT, in fact, sounds just like the description of a package: a collection of data and operations on that data.
Every application you build is filled with the need and potential for abstract data types. In order to construct an ADT, however, you first need to be able to identify which data is best represented at a new and higher level of abstraction. You then need to be able to build that ADT so that the programmatic interface to the underlying data is consistent and effective.
You should build an ADT in four phases:
Clarify the data to be represented by the ADT. Make sure you understand all the attributes of the data and the way it is used in your application. When your ADT is based on a database table or entity, much of this phase is already done.
List the key features and functions of the ADT as it relates to the underlying data . Don't write any code until you have come up with a comprehensive list of all the operations on and by the ADT.
Build the interface for the ADT. This is equivalent to designing the specification of all the modules which are called by programmers to manipulate the ADT. Translate the list of features into procedure and function calls. You need to decide on module names and their parameter lists.
Build the body of code behind the interface. You know how programmers will manage the ADT, since they must work with the specification. The final phase is to actually implement the ADT. What internal data structures will you use? What code will you share among the different modules of the ADT? What data will you hide from view?
Here are some guidelines you should follow when designing an ADT:
Maintain a consistent level of abstraction. This is probably the most important aspect of your ADT implementation. All the modules you build to represent your abstract data structure should operate with the same level of data. If the ADT representing a company's product is implemented as a PL/SQL record, then all of the modules should accept or return this record structure. If there are alternative means of identifying a particular product (the product name, the ID number, the record as a whole), then provide functions that convert between the different identifier formats so that the programmer can easily shift to the ADT structure required by the main modules.
Provide a comprehensive interface to the abstract data type. Make sure that the user (a programmer, in this case) can perform all necessary operations on the ADT without having to go around the interface you build to the ADT. Hide all the implementational details of your ADT behind calls to procedures and modules -- without exception. If you implement a stack data structure as an ADT and forget to provide a way to pop a value off the stack, the programmer will have to invade your turf and mess around in your stack to retrieve the needed value. Of course, this is an obvious example. In many situations the gaps in an ADT's implementation are less obvious and sometimes discovered only when a programmer starts using the ADT in earnest.
Use the package structure, the most natural repository for ADT code. The ADT represents a "thing" by presenting a layer of code that allows you to perform operations on that thing as a whole, rather than on its individual components. The package joins related objects together and so corresponds closely to the ADT. If your ADT represents a caller, then create a package named "caller" containing all the modules, variables, cursors, etc., needed to manipulate the caller. The package clearly distinguishes between the public and private parts of the code. The public objects make up the interface to the ADT. The private objects contain and hide the implementational details for the ADT.
The advantages of working with abstract data types should become clear from the following example of a "progress box". The development team of a financials application found that their Oracle Forms 4.0 application would not change the cursor from an arrow to an hourglass with long-running PL/SQL programs. With a wait-time of between three and five minutes, they were concerned that users would get impatient, click irresponsibly with the mouse, and possibly cause damage. Their solution was to construct a progress box aimed at keeping the user informed of the program's progress.
The progress box consisted of a window named progress_window, a canvas view named cv_progress, and the following three items, all defined in a block named b_progress:
The description of the program currently executing
A textual description of the percentage of the job completed
A graphical representation of the percentage of the job completed
A very clever programmer found that by setting the visual attributes (specifically the font) of the thermometer in the item, the letter "n" would appear as a solid box (blue!) on the screen. Two letters would appear as two blocks and so on. Make the item ten characters long and you have a graphical representation of percentage completion in 10% increments.
Here are the four lines of code needed to show 20% completed:
:B_PROGRESS.PERCENT_DONE := '20 % Complete.'; :B_PROGRESS.THERMOMETER := 'nn'; Show_View('CV_PROGRESS'); synchronize;
Actually, before any particular percentage of completion was displayed, it was necessary to initialize the progress box. The following five lines set the process name (in effect, the title for the bar) and initialized the other items:
SET_WINDOW_PROPERTY ('PROGRESS_WINDOW', Visible, Property_On); :C_CONTROL.NBT_PROCESS_NAME := 'Updating Profit and Loss data...'; :C_CONTROL.NBT_PERCENT_DONE := '0 % Complete.'; :C_CONTROL.NBT_THERMOMETER := 'n'; Show_View('CV_PROGRESS'); synchronize;
This progress box was also needed within a loop, which executed over a period of up to ten years. The development team was able to adapt their basic progress box code to dynamically adjust according to the loop index, year_count:
:B_PROGRESS.PERCENT_DONE := TO_CHAR (round((year_count / years_displayed * 100),0)) || ' % Complete.'; :B_PROGRESS.THERMOMETER := :B_PROGRESS.THERMOMETER || 'n'; Show_View('CV_PROGRESS'); synchronize;
In this case, they simply concatenated another "n" onto the current value of the thermometer for each iteration of the loop.
Of course, they needed to hide the progress box when the program ended:
Hide_View('CV_PROGRESS'); SET_WINDOW_PROPERTY('PROGRESS_WINDOW', Visible, Property_Off); synchronize;
Finally, there were also times when they would use the progress box to display a message, but not use the thermometer. The following code shows the lines required to implement this variation on the progress box:
SET_WINDOW_PROPERTY('PROGRESS_WINDOW', Visible, Property_On); :B_PROGRESS.PROCESS_NAME := 'Building reports...'; :B_PROGRESS.PERCENT_DONE := null; :B_PROGRESS.THERMOMETER := null; Show_View('CV_PROGRESS'); synchronize;
My first impression of their approach was that it was very clever. It had a nice look to it and definitely saved the users from wondering what was happening in their application. Then I took a look inside the form and found that the samples of code I presented previously were repeated over and over again in various forms in many different procedures, triggers, and functions.
The developers had not treated the progress box as a thing in itself, as an abstract data type. Instead, the progress was simply a series of executable statements: set the title, change the thermometer setting, make the view visible, etc. Because they did not abstract those individual statements into an object with rules and structure, they were forced to repeat those statements wherever a progress box of any form was needed. This approach not only required repetition of the statements, it also exposed the particular elements of the implementation (the fact that the letter "n" was needed to created a box on the screen, for example) throughout the application.
Well, the developers worked through these issues and got their application moved into production and everything seemed fine. Then a couple of things happened. First, I was asked to review the code with the objective of reducing code volume and improving performance. Second, we converted the application to Oracle Forms 4.5.
Replacing the sequence of executable statements used to produce the progress box with a procedure call was an obvious way to reduce the amount of code in the application. But the conversion to Oracle Forms 4.5 raised another whole issue: the font used to produce a graphical box from the letter "n" wasn't available any longer in the list of fonts for visual attributes! All the individual assignments of the thermometer to the letter "n" would now do little more than make a blue "n" appear on the screen.
These two factors forced us to completely revamp the progress box implementation. Instead of representing the progress box with the separate lines of code, we needed to create an object called a progress box, figure out the ways that progress box was used in the application, and create modules that would implement those different methods. I wanted to be particularly careful to not let individual programmers hardcode a letter or symbol, like "n", in order to achieve the desired effect in the thermometer item.
After some analysis, I determined that the application used the progress box in three ways:
To display a message (which turns off the display of the thermometer bar)
To display a percentage completion message with the thermometer
To hide the box from view
I then converted this high-level specification to a package specification, as shown in the next section.
/* filename on companion disk: progress.fpp */ PACKAGE progress IS PROCEDURE msg (msg_in IN VARCHAR2); PROCEDURE bar (pct_in IN INTEGER, thermom_in IN INTEGER := 1, init_in IN VARCHAR2 := 'NOINIT', msg_in IN VARCHAR2 := NULL); PROCEDURE hide (item_prop_in IN INTEGER := PROPERTY_OFF); END progress;
The msg procedure simply displays the provided message and hides the thermometer.
The bar procedures takes four parameters as follows:
The percentage completed as a number. The module then converts the number to a string in the format "20% Completed."
The number of boxes to be added to the current thermometer setting.
Indicates whether the thermometer should be initialized with the thermom_in number of boxes (`INIT'), or if the thermometer should have that number of boxes concatenated to the current thermometer display.
The message to be displayed above the thermometer, if any.
The hide procedure hides the property box and optionally sets the property of the thermometer item.
The package body needed to consolidate all of the formerly hardcoded executable statements in the application is shown in the next section. Notice that programmers do not specify a character, like "n", when calling the progress.bar procedure. Instead, they just indicate the number of boxes to add to the thermometer. Inside the package, a one-character string named thermom_char is set to a value of "n". If the font requires a change, this single variable is modified and none of the code that calls progress.bar is affected.
/* filename on companion disk: progress.fpp */ PACKAGE BODY progress IS thermom_char VARCHAR2(1) := 'n'; PROCEDURE msg (msg_in IN VARCHAR2) IS BEGIN SET_WINDOW_PROPERTY (handles.progress_window_id, Visible, Property_On); SET_ITEM_PROPERTY (handles.thermometer_id, DISPLAYED, PROPERTY_OFF); :b_progress.percent_done := msg_in; :b_progress.process_name := NULL; :b_progress.thermometer := NULL; SHOW_VIEW (handles.cv_progress_id); synchronize; END; PROCEDURE bar (pct_in IN INTEGER, thermom_in IN INTEGER := 1, init_in IN VARCHAR2 := 'NOINIT', msg_in IN VARCHAR2 := NULL) IS /* Create the string to be added to the thermometer. */ bar_stg VARCHAR2(80) := LPAD (thermom_char, thermom_in, thermom_char); BEGIN SET_WINDOW_PROPERTY (handles.progress_window_id, Visible, PROPERTY_ON); SET_ITEM_PROPERTY (handles.thermometer_id, DISPLAYED, PROPERTY_ON); IF msg_in IS NOT NULL THEN :b_progress.process_name := msg_in; END IF; :b_progress.percent_done := TO_CHAR (pct_in) || ' % Complete'; IF UPPER (init_in) = 'INIT' THEN :b_progress.thermometer := bar_stg; ELSE :b_progress.thermometer := :b_progress.thermometer || bar_stg; END IF; SHOW_VIEW (handles.cv_progress_id); synchronize; END; PROCEDURE hide (item_prop_in IN INTEGER := PROPERTY_OFF) IS BEGIN Hide_View (handles.cv_progress_id); SET_ITEM_PROPERTY (handles.thermometer_id, DISPLAYED, item_prop_in); SET_WINDOW_PROPERTY (handles.progress_window_id, Visible, PROPERTY_OFF); synchronize; END; END progress;
Now I can replace the cumbersome, virtually unmaintainable sequences of statements with a single call to the package modules. The following examples show the "before and after" of the progress box code.
Initialize the thermometer with a title, empty thermometer, and 0% completed:
Before:
:B_PROGRESS.PROCESS_NAME := 'Updating profit and loss data...'; :B_PROGRESS.PERCENT_DONE := '0 % Complete.'; :B_PROGRESS.THERMOMETER := NULL; Show_View('CV_PROGRESS'); synchronize;
After:
progress.bar (0, 0, 'INIT', 'Updating Profit and Loss data...');
Set the thermometer to 20% completion:
Before:
:B_PROGRESS.PERCENT_DONE := '20 % Complete.'; :B_PROGRESS.THERMOMETER := 'nn'; Show_View('CV_PROGRESS'); synchronize;
After:
progress.bar (20, 2);
Adjust dynamically the contents of the progress box according to the loop index, year_count:
Before:
:B_PROGRESS.PERCENT_DONE := TO_CHAR (round((year_count / years_displayed * 100),0)) || ' % Complete.'; :B_PROGRESS.THERMOMETER := :B_PROGRESS.THERMOMETER || 'n'; Show_View('CV_PROGRESS'); synchronize;
After:
progress.bar (ROUND((year_count / years_displayed * 100),0), 1);
Hide the progress box when the program completed and control was returned to the user:
Before:
Hide_View('CV_PROGRESS'); SET_WINDOW_PROPERTY('PROGRESS_WINDOW', Visible, Property_Off); synchronize;
After:
progress.hide;
By treating the progress box as an object with rules governing its use and appearance, I was able to greatly reduce the volume of code required. The resulting statements are also much more comprehensible and maintainable.
Copyright (c) 2000 O'Reilly & Associates. All rights reserved.