Applying the Object-Oriented Programming Technique of Encapsulation to MultiValue Code - Part 2

This article provides examples of coding an encapsulated object. We continue from part one and show how commuter modules and variable named common areas together provide encapsulation.

We begin by walking through the framework of an encapsulated object, stepping through how to implement protected variables and methods, and finally a wrap-up end-user example of how to create, manipulate, and destroy the object.

Example Code

The following section walks though the construction of a simple invoice object named oInvoice using Revelation Basic+ code. All objects share a similar design framework (template). The purpose of this example is to point out the key parts to the framework and implementation of the object.

The framework section will demonstrate how new instances are setup and kept separate from existing instances. During the implementation section, the oInvoice object will be coded to set the customer name, return the customer name, add items to the invoice, count the number of items, and total the invoice amount.

Once the oInvoice object framework and implementation are completed, another example will be provided to show the implementation of the object from the perspective of a programmer using the oInvoice object.

Object Code Framework

To begin we create a new function for our object. See figure 1. The function declaration is the enclosure for our object and will contain methods (internal subroutines), private variables (variable named common), static variables (general common), and references to supporting routines.

Function oInvoice(instance, inMethod, Param1, Param2, Param3, Param4, Param5, outValue, outStat)

common /oInvoice/ instance_handles@, instances_created@

Equ OBJECT_TYPE$ To "oInvoice"
isNewObject = FALSE$

If inMethod _eqc "Setup" And instance = "" Then
isNewObject = TRUE$
instances_created@ = instances_created@ + 1
instance = time() : rnd(time()) : instances_created@
instance_handles@<-1> = instance
End

inst_handle = OBJECT_TYPE$ : instance
common //inst_handle// invoice_number@, customer_name@, purchased_items@

If isNewObject = TRUE$ Then
invoice_number@ = time()
customer_name@ = "Revelation Person"
Return instance
End

Begin Case
Case inMethod _EQC "setName" ; Gosub pubSetName
Case inMethod _EQC "getName" ; Gosub pubGetName
Case inMethod _EQC "addItem" ; Gosub pubAddInvoiceItem
Case inMethod _EQC "getItemCount" ; Gosub pubGetInvoiceItemCountCase inMethod _EQC "getInvoiceTotal" ; Gosub pubGetInvoiceTotal
Case inMethod _EQC "Destroy" ; Gosub privDestroyThisObject
Case Otherwise$
Call Set_Status(TRUE$, "SYS1000", OBJECT_TYPE$ : " '" : |
                      inMethod : "' is not implemented in object.")
Retval = ""
Goto End
End Case
End:
Return retval

Fig. 1


Before delving into the framework, first consider the interface to the object (the function). Each of the function parameters is described below. 
  • instance - Each object should be unique from the other objects of the same name. The instance variable contains an identifier that relates to a specific object. As you'll see shortly, the variable named common feature is used to facilitate this.
  • inMethod - For an object to be useful, it must be mutable. The inMethod contains the action we want to perform in the object, i.e., get or set a variable (property), return a calculation, etc. Think of inMethod as the internal subroutine in a commuter module.
  • Param1…5 - Parameters that are overloaded depending on the value of inMethod.
  • outValue, outStat - Optional call by reference variables are reserved for complex methods that may need to return additional data.

The first requirement for the object is a common area across all instances of the object. This is accomplished with a normal common area.

Before any object can be useful it must be instantiated. Instantiation is the term used to describe the process of taking a general object and making a unique instance. Generally, a new or setup method will be a reserved keyword enforced by the compiler to accomplish object instantiation. In this environment, there is no compiler directive to require setup to be the method name for creating new instances of the object. The example will continue the practice of using a method named Setup to instantiate a new object.

When inMethod is setup, our object will be instantiated inside the if statement. Instance is assigned a unique value and recorded in the common instance_handles@ variable. To insure our objects are created with unique names the variable instances_created@ tracks the number of objects created.

Now that we have a way to track the objects and assign unique names, the next step is to use the variable named common feature in the Basic+ compiler to assign a common area for this object.

The variable named common area in figure 1 is different from a normal common because it is declared with two sets of //'s and the enclosed name is a variable instead of a literal. This private common provides three private variables for each instance of oInvoice. Although they are technically common variables and can be accessed anywhere in the program, they are protected. Without the correct value of instance (common name), these variables can't be referenced from anywhere else.

The last step of instantiating a new object is to initialize any object specific variables. For the sake of brevity, the invoice_number@ is assigned to be a number based on time and the customer_name@ is set to a generic value.

The variables invoice_number@ and customer_name@ refer to the variable named common area for this instance of the object. Our object now contains enough code to be instantiated into an object.

Implementing Object Methods

To make the object useful, additional methods must be added to manipulate the state of the object. The case block at the end of figure 1 describes all of the methods the object encapsulates.

Like a commuter module, this case block directs program flow to the appropriate internal subroutine (object method). By calling oInvoice with different values of inMethod, the object will perform different methods. We will discuss each of these methods (gosub branches) and look at the example code.

To begin, it would be useful to set and get the name associated with this invoice object. Figure 2 contains all of the subroutine source code inside oInvoice.

pubSetName:
customer_name@ = Param1
Return
pubGetName:
retval = customer_name@
Return

pubAddInvoiceItem:
entry = ""
entry<SKU$> = Param1
entry<DESCRIPTION$> = Param2
entry<COST$> = Param3
Swap @FM with @VM In entry
purchased_items@<-1> = entry
Return
pubGetInvoiceItemCount:
retval = DCount(purchased_items@, @FM)
Return
pubGetInvoiceTotal:
itemcount = oInvoice(instance, "getItemCount")
cost = 0
For i = 1 To itemcount
cost = cost + purchased_items@<i, COST$>
Next
retval = cost
Return

privDestroyThisObject:
instance_handles_new = ""
position = 1 ; flag  = ""
Loop
Remove current_element From instance_handles@ At position Setting flag
If current_element NE instance Then
instance_handles_new<-1> = current_element
End
While flag
Repeat
instance_handles@ = instance_handles_new
inst_handle = OBJECT_TYPE$ : instance
FreeCommon inst_handle
return

Fig. 2

The subroutines to get and set the customer name are straight forward. pubSetName subroutine handles the setName method of the object. For simplicity's sake, data validation has been omitted. Likewise, pubGetName subroutine handles the getName method which simply returns the name assigned to the variable assigned common.

If you've worked with object-oriented languages before, you can see this mechanism provides an interface without revealing the implementation details of where the name is stored. Instead of saving the variable in a named common, we could do a read/write to the invoice record or some other data source.

Take a look at the next three methods in figure 2: addItem, getItemCount, and getInvoiceTotal.

In pubAddInvoiceItem, the subroutine variables Param1, Param2, and Param3 (from figure 1) are pre-arranged by your own good programming practices to be the sku, description, and cost fields. To make the code more readable, the fields are assigned labels using equates. The key to this subroutine is the assignment of the invoice item into the purchased_items@ variable named common.

By calling the subroutines pubGetInvoiceItemCount and pubGetInvoiceTotal the object (i.e., this function) can return data without revealing the internal operation of how pubAddInvoiceItem was used to store the information.

Another powerful benefit of this design is the ability for objects to call themselves to minimize code duplication, even inside of the object. Consider the line itemcount = oInvoice(instance, "getItemCount") in the pubGetInvoiceTotal subroutine (Fig. 2). The subroutine pubGetInvoiceTotal makes another call to the oInvoice object to run the method getItemCount.

The instance variable refers to the current object and, because the object references a common area, any calls to the parent object with the same instance handle can change the state of the object or simply return information about the object state. By creating a method for counting the number of items on the invoice we can leverage it both inside and outside of the object without duplicating the implementation code.

Let's consider another reason for creating a method for something as simple as counting the number of items on the invoice. The DCount function is the standard MultiValue routine for counting the number of fields in a variable. To someone with an object-oriented mindset, functions like DCount are troublesome because the usage of DCount in this situation requires implementation knowledge of the object.

In the example, purchased_items@ contains all of the items on the invoice and, using DCount to count the fields, will return the number of invoice items. What if the oInvoice object included a new variable named invoice_items, would it be evident which variable contains the information to count? Does purchased_items@ contain past purchases or current purchases? Does invoice_items represent the total quantity or just the items number of unique items? The answer is not clear and requires code analysis by the programmer.

The object-oriented mindset is that the object should be able to return the number of items listed without knowing which variable to DCount. This is exactly what the method getItemCount is used for.

The last method in the main case block is Destroy, which is handled by the subroutine privDestoryThisObject. The compiler doesn't implement the concept of objects, so we have to clean up the variable named common when the object is no longer needed.

The subroutine searches all of the instance handles in instance_handles@ for this type of object. Everything but the current instance is assigned back to instance_handles@ thereby removing it from the known list of instances. The last step in the routine is to do a FreeCommon on the variable named common that was established when the object was originally instantiated.

Example of Object Implementation

Everything leading up to this point in the example gets pulled together when we use the object and leverage the methods provided. In this stage of the example, we will make an invoice for Rev Guy, Rev Gal, and Rev Kid. Each of them will buy something and then compile the purchases into a single string describing the amount of their purchase. See figure 3 for the code snippet a programmer would need to use the oInvoice object.

invA = oInvoice("", "Setup")
invB = oInvoice("", "Setup")
invC = oInvoice("", "Setup")

Call oInvoice(invA, "setName", "Rev Guy")
Call oInvoice(invB, "setName", "Rev Gal")
Call oInvoice(invC, "setName", "Rev Kid")

Call oInvoice(invA, "addItem", "AUT123", "Car Widget", 150)
Call oInvoice(invB, "addItem", "SKU123", "Fancy Widget", 20)
Call oInvoice(invB, "addItem", "SKU456", "Cool Widget", 15)
Call oInvoice(invB, "addItem", "SKU100", "Widget Cleaner", 5)
Call oInvoice(invC, "addItem", "TOY932", "Widget Blocks", 10)

PurchaseSummary = |
oInvoice(invA, "getName") : ":" : oInvoice(invA, "getInvoiceTotal") : " " : |
oInvoice(invB, "getName") : ":" : oInvoice(invB, "getInvoiceTotal") : " " : | oInvoice(invC, "getName") : ":" : oInvoice(invC, "getInvoiceTotal")

Call oInvoice(invA, "Destroy")
Call oInvoice(invB, "Destroy")
Call oInvoice(invC, "Destroy")

Fig. 3 

The first step is to setup the objects and assign the unique instance handles into invA, invB, and invC using the Setup method. Now that we have three separate invoices each can have a unique name. The setName method assigns a name to each invoice. Remember, in figure 1, the Setup method assigns a default name of Revelation Person for all new invoice objects.

To populate the invoices, the addItem method will be used to pass information about the item purchased. Notice how the third parameter was a name for the setName method, but is used as a sku number for the addItem method.

The final step is to prove each instance of oInvoice is unique. When the PurchaseSummary variable is assigned at the bottom of figure 3, the uniqueness becomes evident. If successful, it will contain the string "Rev Guy:150 Rev Gal:40 Rev Kid:10".

This example demonstrated that the oInvoice object allows the use of the invoice object without having to deal with the specific internal implementation details. The business purpose behind the code is easy to understand because the implementation details are encapsulated in oInvoice. For complete source code, please e-mail jared@revelation.com.

Conclusion

The purpose of this article was to open your mind to using the object-oriented principle of encapsulation. An example was given to demonstrate how encapsulation can be implemented by leveraging the long used MultiValue technique of commuter modules and variable named common variables. The implementation of encapsulation resulted in high level code that was easy to read, easy to use, and isolated from other routines.

The example also demonstrated how multiple instances of a function (object) could exist during runtime. This is invaluable when extending applications to the web. By design, well-written web applications are separated into layers. These layers are often thought of as objects. In the context of a web application, do you think of the database as a layer or an object? Perhaps the data source is more than one type of object?

Another benefit of object-oriented programming is objects are easier to save and reference across sessions because the variables that compose a specific instance are well defined.

At the heart of MultiValued platforms is a natural mindset of object-oriented thinking; tables provide dictionary fields to hide calculations, commuter modules combine functions to form libraries, and. by their very nature, multivalued records are extensible to fit the business purpose. Now you can include encapsulation to further your techniques.

Revelation Software

Located in WESTWOOD NJ.

View more articles

Featured:

Mar/Apr 2010

menu
menu