Fundamental Visual Prolog - the Business Logical Layer
|Fundamental Visual Prolog|
In this tutorial, we shall modify the earlier simple GUI (Graphical User Interface) front end to the family tutorial which was developed in the tutorial "Fundamental Visual Prolog - GUI". We shall isolate the logic of the code from the rest of the GUI code. This logic would be reposed into an object belonging to its own package. We shall see that this method of isolating the code would enable us to clearly think about the logical issues separately from that of the GUI.
The source files of the example project used in this tutorial are part of the installable examples (in the IDE: Help -> Install Examples...).
In this tutorial, you would need to setup the project's GUI exactly as explained in the "Fundamental Visual Prolog - GUI" tutorial in a series of sequential steps before the section titled "Getting the Main Code into the Program". We shall assume that you have performed those GUI construction procedures correctly, and hence they are not repeated here.
Important: We are also assuming that you have created the AncestorDialog dialog in the GUI and given the code, so that that module has defined a predicate called getName to retrieve the name of the ancestor entered in that dialog.
We will assume that the project is entitled family3.
The GUI of the program is technically known as the presentation layer. Earlier, the code for the logic was intermingled within that presentation layer. Now, we shall take that logic and put it into its own package within the project. That package would act as a business logic layer.
Note that we are using the phrase "business logic" instead of merely the "logic", because it indicates the program's business side. In short, the raison-de-etre of the programis taken care of by that layer.
As Visual Prolog is a strongly object-oriented language, the business logic would now be represented by an object. This tutorial cannot get into all the details of object oriented programming present in Visual Prolog. They are separately explained in the tutorial "Introduction to Classes and Objects" and you are requested to read that tutorial for details. However, for completeness sake, some ideas regarding object-oriented would be mentioned in this tutorial also, wherever relevant. But before all that, let us complete the program in a step-by-step fashion.
Creating the Business Logic Layer
Once you have setup the GUI of the family3 project, the main project tree would look something like this:
Now right click at the top of the tree and from the menu that is presented click on the New item as shown below:
From the Create Project Item dialog that is presented, select Package (the first item in the list on the left hand side) and give the name of the package as familyBLL (as a mnemonic for Family Business Logic Layer; you should ideally choose something which is meaningful).
On clicking Create the project tree will now look like this:
If you now take a peep into the folder on your hard-disk, where these files are stored, you would notice that the IDE has created a separate folder (called familyBLL) within the main project folder to store all the files of this particular package.
Now, build the project to ensure that everything is in order.
Working on the Business Logic Layer
Let us now put the logic in place. We shall create and design a familyBLL class in the familyBLL package to take full advantage of the object-oriented features of Visual Prolog. In this particular tutorial, not all the features are demonstrable. However, we shall be using one of the main features of an object-oriented system; namely that of data encapsulation. (Complete details on object-oriented features of Visual Prolog are available in the "Introduction to Classes and Objects" tutorial).
The term data encapsulation represents hiding the data within the code of a particular module in such a manner that it is not directly available for reading or modification from other modules of the program. Data access is strictly regulated and is indirectly available for reading and modifications through predicates of the class.
This concept avoids one of the most common programming errors seen in many programming languages. When data are kept lying around (metaphorically) for all programmers working on the project to read or write; it often becomes too tempting for programmers to write quick 'n dirty code to access and modify such data. This can be problematic especially in asynchronous programs such as GUI programs, where data can change unpredictably; often as per the behavior of the people using the program.
Another advantage of data encapsulation is that internal data representation systems can often be modified within a class, without the rest of the code in the project getting affected.
Creating the Business Logic Class
In the Project Tree, right click on the familyBLL package folder. From the menu that opens up, click on New, as seen below:
From the Create Project Item dialog that is presented, create a class called familyBLL. Ensure that the Creates Interface check box is switched on, as seen below:
The moment you click on the Create button, the IDE will present the familyBLL.pro file to you for editing. Replace the default code from implement to end implement to the following:
implement familyBLL open core facts fileName : string. clauses new(Filename) :- filename := Filename, file::consult(Filename, familyDB). clauses save() :- file::save(filename, familyDB). facts - familyDB person : (string Name, gender Gender). parent : (string Person, string Parent). clauses father(Person, Father) :- parent(Person, Father), person(Father, male()). clauses grandfather(Person, Grandfather) :- parent(Person, Parent), father(Parent, Grandfather). clauses ancestor(Person, Ancestor) :- parent(Person, Ancestor). ancestor(Person, Ancestor) :- parent(Person, P1), ancestor(P1, Ancestor). end implement familyBLL
Now, click on the IDE File -> Open... menu item, open the file familyBLL.i, and change the default code seen there with this:
interface familyBLL open core domains gender = female(); male(). predicates father : (string Person, string Father) nondeterm (o,o) (i,o). predicates grandFather : (string Person, string Grandfather) nondeterm (o,o) . predicates ancestor : (string Person, string Ancestor) nondeterm (i,o). predicates save: (). end interface familyBLL
One more file automatically created by the IDE requires to be modified. Click on the File -> Open... menu item in the IDE, and open the familyBLL.cl file.
Before the predicates section you see in that file, you would need to insert the following code:
constructors new : (string Filename).
After inserting the above code, the file will look like this:
class familyBLL : familyBLL constructors new : (string Filename). end class familyBLL
Now build the project and check if everything is okay. It should be, and at this point, even though the code is in place, the project would technically compile into an empty project as we have not put in the glue to connect the GUI to the business logic layer. But before that, let us understand why the code for the familyBLL class was put into three different files.
Understanding the Business Logic Class
If you compare the code that we have written so far with that we wrote in the earlier tutorial, you may arrive at these conclusions:
- The code was broken up into three sections.
- Some predicates and domains were declared in the .i file.
- All implementations were put inside the .pro file.
- The .cl file contained the declaration of a special kind of predicate called a constructor.
When we asked the IDE to create a class for us, we indicated that the class would generate objects. It is important to differentiate the difference between a class and an object.
Metaphorically, a class could be looked upon as a cafeteria that gives out packaged pizzas. Just having such a cafeteria would not ensure that there would be pizzas coming out! One would need to explicitly instruct the cafeteria to send out the pizzas.
Just like the cafeteria, which has the capability of creating pizzas, a class has the capability of creating objects. An object contains code and data that is put together as per the implementation found in the class. Hence the class is analogous to the cafeteria. It can produce the objects. But not just yet!
Just because a class was coded in the project; it does not automatically imply that the project would automatically contain objects of that class. In every class, which is designed to generate objects, there would have to be one or more special constructor/s, which will construct a prepackaged object containing all the relevant code and data for working with the object.
Please note that Visual Prolog also allows creation of classes that does not generate objects, and the code within those classes mainly serve the purpose of modularization. This tutorial is not the right place for discussing such classes that do not create objects.
The constructors of a class are written in the .cl file of the said class. Predicates that are publicly accessible to the entire class as a whole (as opposed to the objects that the class can create) will also be present in that file.
Each object that is created would require to have some publicly accessible predicates to manipulate the data which is encapsulated within it. Such predicates are declared separately in the .i file of the relevant class.
Finally, the implementation file (.pro extension) will contain the actual code. For all practical purposes, none of that code would really be seen by the other modules.
All this work may seem a little laborious. In fact, usually, the opposite is true: the clear cut manner, in which object-oriented programming has been implemented in Visual Prolog, can easily be put to use practically. For example, it is often a good strategy to assign different packages to different programmers, who would stitch up a complex project together with the minimum amount of hurdles, using object-oriented methodologies.
Now having understood all of that, we have to remember one important thing: the project still does not do anything useful simply because the GUI is not connected to the business logic layer. We shall now proceed to rectify that hurdle.
Connecting the Business Logic Layer to the GUI
In the Project Tree, right-click on the TaskWindow.win entry, and select Open as shown below.
In the Code Expert dialog, ensure that default handlers for the id_file_open, id_Query_father, id_Query_grand_father, id_Query_ancestor_of menu items are set, as seen below:
The event handler for id_file_exit for exiting the application correctly would have already been set by the IDE, so you do not have to set it again.
Before we populate the event handlers with the correct code, the presentation layer should contain one fact that will be used to store the object that the program is handling at any point in time.
Hence in the TaskWindow.pro file, the following code must be given:
facts familyBL_db : (familyBLL BL) determ.
The above fact will contain the object representing the family's business logic layer.
For the id_file_open menu item event handler give the following code:
clauses onFileOpen(_Source, _MenuTag) :- Filename = vpiCommonDialogs::getFileName("*.txt", ["Family data files (*.txt)","*.txt", "All files", "*.*"], "Load family database", , ".", _), !, BL = familyBLL::new(Filename), retractAll(familyBL_db(_)), assert(familyBL_db(BL)), stdio::writef("Database % loaded\n", Filename). onFileOpen(_Source, _MenuTag).
Before we write the other event handlers, we need to insert a utility predicate as shown below:
predicates tryGetFamilyBL : () -> familyBLL BL determ. clauses tryGetFamilyBL() = BL :- familyBL_db(BL), !. tryGetFamilyBL() = _ :- stdio::write("No family database loaded\n"), fail.
The above utility predicate is needed for gracefully handling situations, where the user may be attempting to query the family database before the database itself is loaded into the program. As we had indicated in an earlier tutorial, a GUI allows usage of the program as per the user's directions. The sequence of events that the user may undertake can never be fully known by the programmer. In fact, it is a good practice to allow the user to decide what set of activities is to be carried out in a GUI program.
As the program developed in this tutorial also adheres to such an approach, in our little program it is impossible to be really sure whether the user did load the database or not, when the querying is being done. There is always the possibility that the database was not loaded. Hence the program is instructed to use the above utility predicate for giving a graceful message to the user in the case there was no database loaded into the memory.
Now let us concentrate on the rest of the event handlers.
For the id_Query_Father menu item, the following event handler is to be given:
clauses onQueryFather(_Source, _MenuTag) :- stdio::write("\nfather test\n"), foreach BL = tryGetFamilyBL(), BL:father(X,Y) do stdio::writef("% is the father of %\n", Y, X) end foreach.
For the id_Query_Grand_Father menu item, the following event handler code is to be given:
clauses onQueryGrandFather(_Source, _MenuTag) :- stdio::write("\ngrandFather test\n"), foreach BL = tryGetFamilyBL(), BL:grandfather(X, Y) do stdio::writef("% is the grandfather of %\n", Y, X) end foreach.
And lastly, for the id_Query_Ancestor, the following event handler code is to be given:
clauses onQueryAncestorOf(_Source, _MenuTag) :- if X = ancestorDialog::tryGetName(This) then stdio::writef("\nancestor of % test\n", X), foreach BL = tryGetFamilyBL(), BL:ancestor(X, Y) do stdio::writef("% is the ancestor of %\n", Y, X) end foreach end if.
If you notice, all the above three event handlers attempts to retrieve the object under consideration using the tryGetFamilyBL() utility predicate, instead of directly accessing the facts section. In the case the database was not loaded, the utility predicate will fail after giving a graceful message in the Messages window.
Understanding the Listeners
If you examine the event handlers that we have now developed, and compare them with those which were developed in the GUI tutorial, you would notice that there are only small differences.
The most significant one is in the first event handler (onFileOpen) where the program constructs a new object of the program's business layer class and stores that object into the facts section of the module.
Another key point to be noted is the use of publicly accessible methods of the business layer object, which has been stored in the facts section of TaskWindow.pro module.
For example, in the following code snippet, the program retrieves a business logic layer object, signified by the variable BL, and then executes the publicly accessible predicate called grandfather of that object.
clauses .... foreach BL = tryGetFamilyBL(), BL:grandfather(X, Y) do stdio::writef("% is the grandfather of %\n", Y, X) end foreach.
The colon operator ":" between the variable and the predicate is an indicator of that logic. The colon operator which retrieves the publicly accessible predicate of an object is different from the double-colon operator "::", which retrieves a publicly accessible predicate of the entire class. This is seen in the stdio::writef(...) predicate call in the above code snippet. Note that the double-colon operator cannot be used on objects and neither can the colon operator be used on classes.
Running the Code
At this point, we can now compile and run the program. If there are any errors, then I would request you to re-read this code and maybe the earlier tutorial also and check for any errors in your program. One point to be noted is that while setting the event handlers, do not change an event handler name for the event for which a hander was already set earlier.
When you run this program, you would notice that the program functions exactly in the same manner as before. This is one of the fundamental beauties of object oriented programming: it allows quite a lot of internal coding changes without the final functionality changing.
Some More Details
In this system of object-oriented programming class facts are changed into object facts, as each object would encapsulate its own facts.
Let us use once again the cafeteria example and make an analogy of facts with the pizza toppings. This situation is therefore like stating that the toppings on each pizza would be dispatched along with each individual pizza. The toppings would not be left behind at the cafeteria else all the customers would have to come back to the cafeteria to use the toppings! Fortunately, it does make more sense to send the toppings along with the pizza. So is the case with object-oriented programming. It often makes sense to package the facts into the objects of a class rather than put the facts into the class itself.
When we use objects to represent the data, the maintenance of the data becomes much easier. If we do not use an object-oriented approach we may require to clean up the remnants of the earlier data by using the retract predicate (retract is a built-in predicate of Visual Prolog). However, retract is not needed now because a new object is created on load and the old object will be automatically removed by the program using a process called garbage-collection. This happens automatically in the background during some known safe conditions without affecting the user.
When declaring predicates the flows of the parameters to a predicate must be specifically added to predicate declarations, the way we did in the .i file. The anyflow flow-pattern cannot be used on global entities.
In the code implemented in the business layer, we had implemented a predicate for saving the data back to a file. If you note, the save predicate does not need a file name, because the file name is stored in the object. It is left as an exercise to you, to connect this functionality to some suitable GUI event handlers.
In this tutorial we learnt that a finer separation of business logic and its data from the presentation (i.e. GUI) is not only possible but is strongly recommended. Visual Prolog can intelligently assist us create a package containing all the classes that are needed to manage this separation. In this particular tutorial, one class was developed in the package, which achieved this objective. Such a class could create objects that encapsulated both the data and business logic. Other modules are able to function only on a need-to-know basis regarding the business logic layer using such objects. This makes the maintenance and modular development of software much more easier and less error prone.