Fundamental Visual Prolog - the Data Layer

From wiki.visual-prolog.com

Revision as of 09:55, 18 November 2008 by Thomas Linder Puls (talk | contribs) (image indent)


In this tutorial, we will continue with the family example, where we would introduce one more layer - the Data layer - to handle the problem more efficiently. This tutorial will give you all the sophistication that is required to handle complex situations.

Download source files of the example project used in this tutorial:

Concepts

There is a Chinese proverb that goes like this: "Give a man a fish and you feed him for a day. Teach a man to fish and you feed him for a lifetime". There is a lesson in programming there. It is often better to substitute data (a fish) with a method (the procedure of fishing) that can lead to the data.

This concept can be extended further a bit more. For example, if before teaching the person fishing directly, the man is taught the principles of getting food from nature, then the person has a much better chance of getting food for his family.

But here is a caveat: explaining things using analogies can be a bit problematic, because, when you stretch an analogy too far, it breaks! In the above example, if you give the fisherman one more layer, then he may die of starvation before he can harvest the fruits of his fine education!

This manner of daisy-chaining together a set of activities to achieve the end goal can be done in programming also. But then, this has to be done intelligently, as per the demands posed by the complexity of the software problem you are trying to handle. Though it can be said that more the layers that are introduced, better is the control on reaching the eventual goal; one needs to setup the layers after careful foresight.

If you see the pattern that had evolved in through the entire family series of tutorials (Fundamental Visual Prolog, Fundamental Visual Prolog - GUI, Fundamental Visual Prolog - the Business Logical Layer), you would notice the same thing: more and more refined layers were introduced that finely controlled the behavior of the program. At the same time, there were no unnecessary layers - if it were so, it would have only increased the complexity and cumbersomeness of the program.

In this tutorial, we will introduce the Data layer. The core of the data layer is nothing but a class, and objects of that class would be used to access and set the actual data. In the earlier tutorial, please note that the Business Logical Layer (BLL) handled the data also. This time, the BLL would be handling only the logic.

The Program

But first of all, let us understand the various parts of the program with the help of the supplied family4.zip file. In this tutorial, we are not presenting the code that is needed (else this tutorial will become too lengthy). All the code is present in the example for the tutorial. You can read the code from the IDE, when you load the project there. Some partial code is, however, present here, for quick reference.

When you load the project (family4) in the Visual Prolog IDE, you will notice four separate packages as seen below:

Visual Prolog Project window

These packages are the FamilyBLL, FamilyData, FamilyDL and TaskWindow packages. The last package (i.e. TaskWindow) is automatically created by the IDE, when you create the project. The method to create the other packages was explained in a previous tutorial.

The project was created in a similar fashion that was explained in a previous tutorial. There are no surprises there. But here is a bit of explanation of something new.

There is one more dialog that is introduced in the TaskWindow package as compared to the previous tutorial. This is the NewPersonDialog, which is shown below. (We've already included this dialog in the project. However, the explanation below pretends that you are creating this dialog on your own in the project.)

New Person Dialog

This dialog is a modeless dialog, which is used to collect the information of a new person that is to be inserted into the current database. As you can see in the above image, there is a text-edit field (idc_name) for collecting the name, a radio button series (idc_male and idc_female) for finding the gender, and two further text edit fields (idc_parent1 and idc_parent2) to find the parents of the person being added into the database. A modeless dialog is one, which can remain open and active, without interfering in the workings of other parts of the GUI.

There is no difference between a modal and modeless dialog when it is being created. However, when you set its attributes you must be careful to specify that it is indeed a modeless one, as shown below:

Specifying

Now just creating such a dialog is not of any use if there is no way by which the dialog can be invoked from TaskWindow. Hence we need to insert a new menu item in that and associate an event handler with that menu item. Once we create the appropriate menu item, the code wizard will allow us to create a handler into which we can fill in the requisite code. The image is shown below:

Creating a handler in the Dialog and Window Expert (TaskWindow)

Please note that in an earlier tutorial (Fundamental Visual Prolog - GUI), we had shown how to create an event handler for a menu-item using the above code wizard.

Modeless Dialog: Conceptual Issues

This program uses a modeless dialog in order to insert information regarding new persons into the family database. As indicated in an earlier tutorial, coding for the events happening from a modeless dialog can sometimes be tricky. This can be understood if you know exactly why a modal dialog works the way it does. In a modal dialog, the operating system suspends events happening from all other GUI parts of the same application and concentrates only on those that emerge from the modal dialog. The logic required to be handled by the programmer at that point in time becomes quite simple, as the programmer is quite sure that there are no untoward events that are happening simultaneously in other parts of the same program.

On the other hand, when a modeless dialog is in use, the program is receptive to events even from parts of the program along with those occurring from within the said dialog. The permutations and combinations of event handling required need to be carefully thought through by the programmer. For example, the program could open a database, and then have a modeless dialog opened, which worked on the database. It would throw an error, when the dialog is put to use if in the background the database was closed without shutting down the dialog. Unless, of course, you had anticipated such a course of events and programmed for such situations.

If you see the source code that handles the events happening from this dialog in NewPersonDialog.pro, you would see that the code carefully catches a lot of error situations. It even has an integrity checker that attempts to trace what may cause a piece of code to cause an error.

Predicates that may lead to errors during runtime are called from within a special built-in predicate called trap. For example, see the following code:

clauses
    onControlOK(_Ctrl, _CtrlType, _CtrlWin, _CtrlInfo) = handled(0) :-
        Name = vpi::winGetText(vpi::winGetCtlHandle(thisWin, idc_name)),
        Name  "",
        MaleBool = vpi::winIsChecked(vpi::winGetCtlHandle(thisWin, idc_male)),
        Gender = maleFlagToGender(MaleBool),
        OptParent1 = optParent(idc_parent1),
        OptParent2 = optParent(idc_parent2),
        try
            bl:addPerson(Name, Gender, OptParent1, OptParent2)
        catch TraceId do
            integrityHandler(TraceId))
        end try,
        !,
        stdIO::write("Inserted ", Name, "\n").
    onControlOK(_Ctrl, _CtrlType, _CtrlWin, _CtrlInfo) = handled(0).

In the above code snippet, the programmer is not sure whether the addition of a person into the database is really possible (because it is not allowed to add an existing person). Thus the addPerson is not called directly, but through the trap predicate. The trap predicate accepts three variables; the first of which is the predicate to be called to do the actual work at that point in the code, the second is the error number that is generated in the case an error is triggered, and the third is a predicate that is called on the triggering of the error. That third predicate can be used by the programmer to bring the program safely back into control.

The FamilyData Package

In this example, we'll create one FamilyData package, which has all the required code for handling the domains that are needed by the program. The same domains would be available to the BLL (Business Logic Layer), when it communicates with the data layer.

In this class, we'll write the following code:

class familyData
    open core
 
domains
    gender = female(); male().
 
domains
    person = string.
    person_list = person*.
 
domains
    optionalPerson = noPerson(); person(person Person).
 
predicates
    toGender : (string GenderString) -> gender Gender.
 
predicates
    toGenderString : (gender Gender) -> string GenderString.
 
predicates
    classInfo : core::classInfo.
end class familyData

If you notice, the above class contains the domain definitions that are to be shared between multiple classes. Thus, the purpose of this package is to act as a meeting place between the Data Layer and the Business Layer.

Interfaces

The layer, which will handle the data, will be via objects created from a class in the FamilyDL package. However, before we get to the actual contents of that package, we need to understand a concept called an interface.

An interface can be looked upon as a statement of intention on what to expect in the objects that would adhere to the said interface. It is written in a separate file using the IDE and it can contain predicates that can be expected to be found in the objects of a class.

An interface can also be used for some more definitions that are not explained in this tutorial. You can learn about those advance usage in a future tutorial.

The reason for writing the intentions separately into a different interface is an elegant one: A class can be written to adhere to the declarations found in an interface. The interface gives an abstract definition of the functionality provided by the objects of the class. The definition is "abstract" because it only expresses some of the functionality of the objects. In short, a class can declaratively state that its objects would be behaving in accordance to the interface.

Several classes can implement the same interface; each of them providing a specific, concrete (i.e. non-abstract) implementation of the abstraction. This reduces the burden of code maintenance tremendously. Later on, when more sophistication is to be introduced in the same program, the programmer/s can work on these interfaces and then work their way from there methodically.

The FamilyDL Package

This example also contains another package called the FamilyDL package. This package contains the main class, whose objects would handle the actual data.

This package contains an interface - the familydl interface. The main purpose of this interface is to define the predicates that are provided by the data layer to the business logic layer. As such it plays a major role in the data layer. This interface will be based on domains from the FamilyData package.

interface familyDL
    open core, familyData
 
predicates
    person_nd : (person Name) nondeterm (o).
 
predicates
    gender : (person Name) -> gender Gender.
 
predicates
    parent_nd : (
        person Person,
        person Parent)
    nondeterm (i,o).
 
predicates
    addPerson : (person Person, gender Gender).
 
predicates
    addParent : (person Person, person Parent).
 
predicates
    personExists : (person Person) determ.
 
predicates
    save : ().
end interface familyDL

In the above code, the open keyword is used to indicate that the declarations in both the core and the familyData packages are used in this interface.

The package contains two classes: family_exception and family_factfile. Most of the work will be done by family_factfile. That file will contain the code, which brings in the fish (if one were to again use the metaphor of the Chinese proverb mentioned in the beginning of this tutorial). It systematically builds up the database that is used by the program, and, while doing, so checks for any error situation that may happen. In case there is an error, it would use predicates from the family_exception class to systematically signal the error.

Code for the familyDL Package

The familyDL package has one interface file familydl.i, which code was given earlier. The package also contains two .pro files containing the implementations of two .cl (class) files. The two classes in this package are familydl_exception and familydl_factfile. The code for familydl_factfile can be examined from the family4.zip file that can be downloaded separately for this tutorial. The predicates in that file are for handling the data, and we have covered that in our earlier tutorial.

The code for familydl_exception is also not presented here in this tutorial. Please read it from the family4.zip file. The predicates contain support utility predicates that are used to show correct error messages, when incorrect data and/or errors (known as exceptions in computerese) are caught by the program. Many of these error situations happen without the control of the programmer (for example, a drive door being open is an exception that cannot be predicted), but sometimes, it is required to make an error happen. That means, the programmer deliberately raises an error (to use some computer jargon here). Thus the error utility predicates start with the term raise_ as you may notice in the code.

Note that familydl_exception does not contain the logic for handling the exceptions themselves. They are utility predicates that allow the program show the correct error dialog, when exceptions are to happen.

Here is an example how these raise_ predicates are deliberately invoked by the programmer to signal an error situation.

In the familydl_factfile.pro you would find a predicate for adding a parent. In the clause body, it first goes through two checks to see if both the Person as well as the Parent of the Person are existing. If anyone of them does not exist, then familyDL_exception::raise_personAlreadyExists is called. This raises the exception and the program would display a special error dialog at that point. Fortunately, that particular error dialog does not need to be constructed by the programmer. The dialog creation work is done by the libraries of Visual Prolog when it links your program. You would not be creating a separate error dialog the way we did for other dialogs.

%note: the following is only partial
%      code from familydl_factfile.pro
 
clauses
    addParent(Person, Parent) :-
        check_personExists(Person),
        check_personExists(Parent),
        assertz(parent_fct(Person, Parent)).
 
clauses
    personExists(Person) :-
        person_fct(Person, _),
        !.
 
predicates
    check_personDoesNotExist : (string Person).
clauses
    check_personDoesNotExist(Person) :-
        personExists(Person),
        !,
        familyDL_exception::raise_personAlreadyExists(classInfo, Person).
    check_personDoesNotExist(_).
 
...

Code for the familyBLL Package

The code for the familyBL.i interface file is given below. This is quite similar to the interface for the business layer that was developed for the previous tutorial. But there are important differences: the domains have been shifted out of this package and are now commonly shared between the Data Layer and the Business Layer. This is an obvious move, because in the previous tutorial we had kept both the Business Layer and the Data Layer as one piece of code. When we separate these two layers, we would still need some common point through which these two layers can talk to one another, and the obvious decision would be to make the domains used by them common to each other.

The second change that can be noticed is that the predicates do not have multiple-flows. Each predicate does exactly one thing with the parameters that it handles. This makes the program move away from traditional Prolog and it becomes similar to programs written in other languages. Often, this strategy yields better results because it makes it easier to understand the program when one reads the source code. However, the former method of having multiple-flows does result in shorter source code and is often preferred by programmers who came in after using traditional Prolog.

interface familyBL
    open core, familyData
 
 
predicates
    personWithFather_nd : (person Person, person Father)
        nondeterm (o,o).
 
predicates
    personWithGrandfather_nd : (person Person, person GrandFather)
        nondeterm (o,o).
 
predicates
    ancestor_nd : (person Person, person Ancestor)
        nondeterm (i,o).
 
predicates
    addPerson : (person Person, gender Gender, optionalPerson Parent1, optionalPerson Parent2).
 
predicates
    save : ().
end interface familyBL

The source code for the familyBL class is not presented here. You may read it once you load the code from the supplied example into your IDE. There should be no surprises there. It does the same activities that were carried out in the business layer of the previous tutorial (Fundamental Visual Prolog - the Business Logical Layer). However, there are two important differences: wherever data accessing or setting of data is needed, predicates from the familyDL class are used. Also, the predicates of this class check the validity of information and raise exceptions in the case logical errors are found.

The package contains one more class: familyBL_exception. This class is similar to the one that we found in the Data Layer. But this time, it contains utility predicates that are used to raise exceptions, when the Business Layer predicates are at work.

Features of a Data Layer

The main feature of a Data layer, is that it implements a set of predicates collectively referred to as data accessors in object-oriented languages. A Data accessor decouples data access from the underlying implementation. In simpler terms, you would use predicates to set and get the data used by the program. The actual data would remain private and inaccessible from the outside.

Data accessors can also provide a uniform interface to access attributes of the data, while leaving the concrete organization of the attributes hidden.

The advantage of using data accessors is that algorithms implemented in other areas of the program can be written independent of the knowledge regarding the exact data structure that was used in the Data layer. In the next tutorial in this series, you would see that the data can be easily shifted to a more robust system (using Microsoft's Access database, accessed through an ODBC layer) while retaining everything else exactly the same.

There are hardly any pitfalls to this approach, but whatever they are, these pitfalls must be understood. The main one is that two sophisticated levels of understanding have to be reached by the programmer when writing the Data layer. One would be to determine the internal data structures that will remain private within the Data layer. (The kind of thinking, required to develop data structures, was discussed in the Fundamental Visual Prolog tutorial regarding functors). But after that, the programmer will also have to spend time designing efficient data accessor predicates so that other layers can mate smoothly with the Data layer. Incorrect design of the data accessor predicates can often create a lot of headache, when a team of programmers is at work. Especially, when the program is required to move from a simple one to a more complex one... as you would notice in the next tutorial.

Conclusion

In this tutorial we used a strategy of "teach the program fishing and not give it fish" in our program. This is nothing but modularizing of the source code of the software intelligently so that code maintenance and extension of the code becomes extremely simple. We split the erstwhile Business Layer of the previous tutorial into two parts: one contained pure business logic and the other one handled just the data. This allows us to extend the program later on and replace just the data layer with other forms of data handling techniques (such as through data retrieved via an ODBC protocol, or via the Internet, etc.) This tutorial also covered exception handling and we discovered, how easy it is to write our own exception raising predicates, and use those to display intelligent error messages to the user.

References