Object Classes and Instances:
The Basics
By David Swain
Polymath Business Systems
One subject that we have received
a number of requests for explanations on is Object Classes. While
some developers are happily propagating objects everywhere, others
are completely baffled and would like some clarification. To that
end, we will set out to make the use of object classes more accessible
to a wider audience.
Interspersed with articles on Object Classes and Instances will
be some articles on Window Classes. We have never really broached
that subject in any meaningful way, yet we have assumed that windows
were well understood in many of the examples provided over the years.
We now know that this is not entirely true. Besides, the introduction
of Object Classes to Omnis Studio offers us some new ways to take
some of the processing burden off the windows in our applications
and centralize some of that processing in reuseable objects that
can be deployed as needed. So we will work along two parallel tracks
for the next few months, hopefully making sense of how each of these
class types work and of how we can use them together in meaningful
ways.
Clarification of Terms
The term "object" is used to refer to several different
things in Omnis Studio, depending on the context of the conversation.
As is so often the case, there just aren't enough words in the language
to give one to each new concept that comes along! So we'll have
to employ a few adjectives and other descriptors to help us distinguish
among these disparate items.
First, we have the term "object orientation". When used
in relation to this concept, all items within the Omnis
Studio domain that have properties and/or methods are "objects".
They are hierarchically strung together in our libraries (each of
which is itself an "object" as well) to form the very
fabric of our applications. This is not the sense in which we will
discuss "objects" in this current series of articles.
We then have "component objects" of windows, remote forms,
menus, toolbars and reports in Omnis Studio. Windows, remote forms
and reports can have "field objects" ($objs group)
and "background objects" ($bobjs group - except
for reports) , menus contain "line objects" ($objs
group) and toolbars contain "tool objects" ($objs
group). Again, this has nothing to do with the concept of an Object
Class or an Object Instance.
The kind of "Object" we will discuss in this article
is a special type of class within Omnis Studio. It is also a special
type of instance that can be spawned from a class of that type.
There are other items bearing the word "object" in their
names that are related to this concept. We will also cover
those subjects here as they become appropriate to the discussion.
So What Is an Object Class?
Of the many class types available in Omnis Studio, the Object class
type is one of the more fascinating. An Object Class is a "faceless"
(non-GUI) collection of methods and variables which is usually deployed
(instantiated) as a scoped variable. We can use it in a number of
ways, but we mainly use it in its instance form. This is
because of the way this kind of "object" is generally
accessed, as we will see later.
In some sense, an Object Class is similar to a Task Class - but
only with regard to what it can contain. The purpose
and use of an Object Class differs markedly from those
of a Task Class, so I hope I don't cause any confusion pointing
out the structural similarities between the two! Both contain methods
and variables, but no component objects - and both are mainly useful
as instances. That is the extent of their shared qualities,
however.
As with many other class types (except Code, Search and the data
description class types), an Object Class is the "blueprint"
for instances. Object Instances are what do the work of this class
type in a running application. But the way that Object Instances
are deployed is quite different from other kinds of instances
we have explored. We don't simply open them or install them in some
visual location.
Spawning an Object Instance
An Object Class isn't of much use until we spawn an Object Instance
from it. We design the Class, but we use the Instance.
One of the intriguing things about Object Classes is that their
instances are expressed and manipulated as variables. We
use a variable of a special data type called "Object"
to contain an instance of an Object Class at runtime. (We will discuss
Object Reference variables in another article.) For right now, let's
focus on the use of instantiated scoped variables (task,
class, instance and local scope domains)
in our initial explorations. File Class variables can also be given
the Object data type, but this is usually for a different purpose
than the one we will focus on here.
There are three techniques that we can use to instantiate an Object
Instance. Here are brief explanations of static, dynamic
and instantaneous instantiation:
Static Instantiation
We can most easily instantiate an Object Instance by creating a
variable of Object type at the appropriate level of scope and then
specifying an Object Class from our library as the subtype for that
variable. This is all done in the Variables Pane of the Method Editor
window for the class to which the Object variable belongs. That
class will most likely be a Window, Report or Task Class, but it
can be any code-bearing class - including an Object Class. (Yes,
we can nest objects inside other objects. More on that subject some
other time!) Here is what completed Object variable definitions
look like:
We select the Object Class for the Subtype column entry by using
a convenient dropdown list that shows all the Object Classes in
our library as well as other items that can be used with this variable
type (more on that later):
When an Object variable with a "complete" definition
is first accessed by our code, it is ready to go. It contains an
Object Instance (an instance of the Object Class specified in the
Subtype column), complete with all of its methods and internal variables.
The Initial Value Calculation for this Object variable (if we supply
one) is evaluated and sent to the first parameter of the $construct
method of the instance (if such a parameter exists there). We cannot
send more than one parameter here because we must supply a valid
expression in the Init. Val/Calc space - and that reduces
to a single value. We cannot supply a comma-delimited value
list here as we might in other places where we send parameters.
If we try to do so and enclose that value list in quotes, it reduces
to a single string value as far as Omnis Studio is concerned, so
still only one parameter is sent.
But we have other options...
Dynamic Instantiation
When we define a variable of Object type, we have the option to
leave the subtype empty. If we had already specified an Object Class
and now want to remove that earlier choice, we can instead choose
the "<None>" option shown in the illustration above.
Of course, the variable isn't at all useful in this state, but we
can populate it dynamically in our code before using it. We perform
this feat using the $new() method of the Object Class with
an Object variable as the recipient of the return value of this
method. Here is an example:
So why would we use this technique when we could simply assign
an Object Class as the subtype in the Variables Pane and be done
with it? I suppose that some developer somewhere might want to swap
instances of different Object Classes into the same Object variable
(and this can be done), but there is a more likely reason: The $new()
method allows us to pass multiple parameters to the $construct
method of the instance. We can't do that in the Variables Pane definition.
The code line above was from the $construct method of a
Window Class and was included there for precisely this reason.
In multiple library situations, a reference to the library that
contains the Object Class must be included in the notation string.
$objects is unambiguous in a single library application,
but not when more than one library is open.
That would seem to be the only two ways to instantiate an Object.
How could there be a third? Well, these are the only two ways to
populate an Object variable, but who says we always need to use
a variable?
Instantaneous Instantiation
There may be occasions when we feel that we don't need to hold
an Object Instance in a variable. Perhaps we only need to use one
of its methods in only one place in the code of a window, for example.
In such a case, we can dynamically spawn the instance, use it and
let it disperse again into the ether without taking up permanent
residence in our Window Instance.
The $new() method can be followed by additional notation
that specifies a public method within the Object Instance it spawns.
The notation string is executed from left to right, so the $new()
method is resolved (an Object instance is created) and any notation
that follows in the same notation string then applies to that instance.
For example:
This line creates a new (and ephemeral) instance of the demoObject
class, passing two parameters to its $construct method.
(The parentheses characters are still required even if we do not
need to pass parameters to the constructor.) It then invokes the
$test1 method of that instance and passes a parameter to
that method as well. The return value generated by $test1
is the result of the expression represented by this notation string
and that result is then assigned to variable.
What happens to the Object Instance? It simply vanishes since we
did not put it anywhere. Wow! If we can do this, then why would
we need to put an Object Instance into an Object variable in the
first place?
Most often, we need persistence of some sort in our Object Instances.
We choose the scope of Object variable to match the persistence
we require. (Instantaneous instances are even less persistent
than local variables.) But even for Function Objects (discussed
below) that are only used as in the example above and do not carry
variable values from one use to the next, there are still good reasons
for using a variable instead of instantaneous instantiation. The
main reason is that instantiation still requires a finite amount
of time to accomplish. If we have a few places in a method where
calls to methods in an Object must be made, each of those would
require that the Object go through the instantiation process if
we were to use this technique exclusively. That must be weighed
against the cost of holding an Object Instance in a variable in
RAM. Even though processor speeds are getting faster all the time,
RAM is also getting cheaper. I still usually choose to use the RAM
rather than the processing time, but it is good to have this option.
Other people will have other opinions.
The Class Variable Difference
When an Object variable that is fully defined in the Variables
Pane first comes into being, all of its public methods become available
for notational access, its internal instance variable slots are
created (and populated with initial values if those have been included
in their definitions) and its $construct method is executed.
This occurs when the Object variable is first accessed by code in
the instance to which it belongs, just as is true for any other
scoped variable.
Most scoped variables cease to exist when the instance (or the
method, in the case of local variables) to which they belong
ceases to exist (or finishes execution). This applies to task,
instance and local variables, but class
variables persist (once they have been created) until the library
to which their parent class belongs is closed. They also partially
come into existence before they are referred to by executing code,
so we can examine them through various means.
What do I mean by partial existence? Simply that the $construct
method is not executed. Let's look at a simple example:
Create an Object Class named "demoObject". Give it an
instance variable named "var1" of character
type and give it an initial value of "testing".
Now create a Window Class named "objectTester". Give
it a class variable named "demoObj" of object
type and make demoObject the subtype Do not open a test
instance of this window just yet, but bring the layout view of objectTester
to the front and open the Catalog.
In the Catalog, select Class from the left list of the
Variables tab. demoObj should appear there. Context-click
(right mouse click on Windows, Control-click on Mac if you have
a one-button mouse) on the variable name and select the first line
of the context menu that appears. This should open the Value Window
for demoObj, which in the case of an Object shows its internal
variables - with the instance variables exposed by default.
Notice that var1 already has its default value of "testing".
But how do we know the $construct method did not execute?
Well, we don't at this point. We have to perform another experiment.
Open demoObject again and cut the initial value calculation
so that it is now empty. (We cut it so we can paste it elsewhere
instead of retyping it - just lazy, I guess.) In the $construct
method, place the following line of code:
This instance variable of our Object must now rely on
the $construct method of that Object for setting its initial
value. But we have already brought our class variable (with
all of its instance variables) into existence and it will
persist until we close the library. So we must close and reopen
our library to see the effect of our changes. After doing this and
testing the demoObj class variable as before, we now see that the
value of the var1 instance variable within demoObj is still empty:
This means that the $construct method, where the command
to populate var1 now resides, did not execute. Maybe we
just need to open a test instance of our objectTester window.
But if we try that, we still get the same result. What to do?
The secret is that we haven't yet accessed demoObj from
our code, so it has no need to construct itself. Let's make a couple
more changes.
In the Object Class, create a method named "$test1" and
give it the following line of code:
Now place a pushbutton object on the objectTester
window and put the following code in its $event method:
If we open a test instance of this window once again, we see that
var1 is still empty. But if we now click the pushbutton
and check again (closing and reopening the Value Window might be
necessary in some cases), we see that var1 now has its
initial value of 'testing'. This is because demoObj was
finally accessed by our code, so Omnis Studio was obliged to construct
it for us and the $construct method executed.
As a byproduct of this experiment, notice that the name of the
Object Instance is, in fact, the name of the variable that contains
it. ($fullname shows us the actual path to that
variable from $root.) Of course, this is only true for
Object Instances contained in Object variables. Instantaneous instances
have no variable from which to draw a name, so they are named using
the class name followed by an underscore character and a number
assigned by Omnis Studio. (We will see a similar behavior when we
explore Object Reference variables in a later article.)
While this Object Instance will continue to exist until the library
is closed, we can't do anything with it if all instances of the
objectTester Window Class are closed. This is because we
can only access a class variable of Object type (for application
execution purposes) through an instance. And that is because of
the way in which we generally use Object Instances - either to use
their internal variables or to send messages using Notation. One
reason we would use a class variable rather than an instance
variable for our Object Instance is that a class variable
is shared among all instances of the class to which it
belongs. Most of us are less likely to use class variables
that instance variables with our Object Instances in Omnis
Studio, but I thought this behavior was otherwise instructive.
What's in a Name?
If we look closely at the variable context menu for demoObj
in the Catalog, we will see something that is easy to miss. While
the name of our instance ($cinst().$name) was
determined above to be "demoObj" (the name of the Object
variable), the value contained in the object variable is
"Object demoObject" (the name of the class from which
the instance was spawned - and probably the real, internal name
of the instance as well):
So maybe an Object Instance held in a variable has two names: its
reference name (the one we use to refer to it) and its instance
name (determined by similar rules to those used to name instances
of other class types). This will get more interesting as we explore
further...
Performing Operations on Object Variables
While there are only three ways to instantiate an Object
Instance, there are a few additional ways in which we can
populate an Object variable. These involve using existing
Object Instances and making copies of them into other variables.
For example, as with most other variables (except reference variables),
we can use the Calculate and Do commands to make
an exact clone of an existing Object Instance and put it
into a different Object variable. Let's do an experiment in the
window we have been using to demonstrate this.
Create a new instance variable of Object type. Name it "newObj"
and leave its subtype empty. Now place another pushbutton field
on the window and put the following code into the $event method
of the pushbutton:
Now open a test instance of the window. First click our original
pushbutton to make sure that demoObj is fully instantiated
(we might want to examine the Variable Value window just to double-check),
then click the new pushbutton. The OK message dialog appears
announcing that the name of the current instance is "newObj",
which indicates that our clone was successfully created.
But if we now examine the variable context menu for newObj,
we see something really interesting:
The value contained within newObj is the name
of the original Object Class followed by an underscore character
and a 3-digit number! This looks like the kind of instance name
we might expect if we were to specify "*" as the instance
name for a window instance. (The variable tooltip for newObj also
says "newObj = Object demoObject_171", both in the Catalog
and the Notation Inspector.)
What is even more interesting is that if we now close the window
instance, reopen it and perform the same operations, demoObj
will now contain a value with the "random" (and slightly
higher) number appended and newObj contains a value of
"demoObject". Without trying to explain this further,
suffice it to say that there is an internal instance name for the
Object instance itself. We simply access the instance in practice
through its container. We will see a similar situation when we discuss
Object Reference variables.
For extra credit, go back and change the Calculate command
to a Do command. Just select the Do command from
the commands list and don't change anything else. (The result should
be Do demoObj Returns newObj.) Testing will show that this
works exactly the same as our original code using Calculate.
Besides using a calculation to clone an Object Instance, we can
also pass the Object variable as a parameter of Object type. Again
the result will be a clone of the original Object Instance (with
a new internal name). While these clones are exact duplicates of
the original at the time they are created, they have a life of their
own from that point onward and are not tied to the original in any
way. This can have its uses, but there are also ways that we can
pass references to objects so that there is only one original that
can be accessed from a number of locations. We just won't go further
down that path for a couple of articles...
Uses of Object Instances
So far we have discussed how we create Object Classes
and Instances without considering why we might want to
do so. In general, we use their public methods and their
instance and/or class variables (which are also publicly exposed)
for various purposes within an application. These methods can be
centrally maintained (in the Object Class) but used in various places
throughout the application (through Object Instances of appropriate
scope).
Why the emphasis on public methods of the instance? Well, in the
Object Oriented way of programming, public methods are the ones
exposed to the outside world. They are our means of communicating
with an instance from outside that instance. Since Object Instances
do not have visible component objects, public methods are our only
way to communicate with an Object Instance.
In Omnis Studio, the public methods of an Object Instance (or any
other instance, for that matter) can be executed like any other
function through the use of Omnis Notation, unlike "universal"
methods held in a Code Class. This means we can invoke these methods
implicitly within any expression, not just explicitly using
the Do command. This offers us tremendous flexibility in
our coding!
We can also spawn multiple instances of an Object Class (perhaps
within multiple instances of a window for which an Object variable
is defined) that can contain different data views in their own instance
variables. The imagination begins to kick in with additional possibilities
the more we learn what can be done with Object Instances! While
the use of Object Instances is a broad subject, we can
focus the discussion a bit.
In my own work, I have identified four categories of uses for Object
Instances. I'm sure this is not an exhaustive list of possible uses,
but it seems like a good place to begin that discussion. My four
categories are Function Objects, Helper Objects, Data Objects and
External Objects. We will spend more time detailing these in subsequent
articles, but here is a brief description of each category:
Function Object
This is perhaps the simplest of the uses for Object Instances.
A function object is a collection of public methods that
return values. We can use them in much the same way as we use the
built-in functions in Omnis Studio, hence the name. In practice,
I group methods that perform related operations (financial functions,
string functions, date functions, etc.) into Object Classes with
names that describe that group and then deploy those functions where
needed. If I need one group of functions in many places throughout
a given task, I deploy the object as a task variable. If
I only need those functions inside certain reports or windows, then
I deploy instances of that object as instance variables
in only those reports or windows that need it.
If I expect to have many instances of the same window
open simultaneously, I might consider using a class variable
instead of an instance variable for such an object. This
is not for the persistence of internal variable values, but for
space saving in RAM (no sense in having lots of copies of the same
method collection floating around if I can help it!)
In the "mathematical" sense, a "function" is
a method that accepts one of more values and returns a resulting
value. While there will be exceptions to this, it is a generally
descriptive rule. This means that a public method in a function
object will usually have at least a Quit method line that
generates a return value and will usually have at least one parameter
variable. We will explore this all in the next article.
Helper Object
This use of Object Instances works more with manipulating objects
within an application than it does with manipulating data values.
A helper object is a collection of methods that perform
actions, generally on objects (lower case "o") in the
application. For example, an Object Instance might be built to manage
multi-window processes. Such an Object might also use instance variables
of its own for maintaining a list of the windows involved in the
process, among other things. Again, we would be advised to cluster
methods with related purposes into the same Object Class.
For those helper objects that help instances of various types communicate
with one another, I prefer to have them instantiated outside the
direct influence of any one of those instances. For that reason,
I often choose to use task variables to house these Object
instances. But there are also a number of helper objects that fit
better of instance variables in the window or report where
they are used.
Data Object
A data object can be much more complex than a member of the two
categories already mentioned. This is a collection of instance variables
and methods that represent data constructs. I have determined two
sub-categories: custom data types and data views.
A custom data type is an Object that generally manages
only one data value (usually either a string or a number), but applies
special formatting or other characteristics to that value. For example,
we might create an Object Class to deal with degree-minute-second
values for latitude and longitude. Or we might create one for handling
SMTPE time code values for video work. Such data objects not only
hold an intermediate display value for conversion to and from a
value stored in the database, but they also contain the methods
needed to perform that conversion as well as to perform various
operations on that value type.
A data view object contains one or more row or
list variables that maintain record images from the database
- often from different files or tables. Such an Object could represent
a single database table or a collection of related tables, complete
with all the business rules for how they relate to one another.
So we could have an Invoice Object, for example, that handles all
the records that comprise an invoice - perhaps spread over half
a dozen tables - and contains all the methods we need to perform
any operation regarding those tables. We could then use an instance
of that Object anywhere within our application where we need to
deal with invoices. The code is maintained in one location, but
we can use that code from within any window or report. This demonstrates
the potential power of Object Instances in Omnis Studio.
External Object
There are a number of external components that are designed to
work as Objects with Omnis Studio. We can create Object Classes
or Object variables that inherit methods and variables (properties)
from these specially-built external components. We can then deploy
instances of these Objects at appropriate levels of scope wherever
we need their assistance.
For example, there is a Timer external that we can use in this
way. This allows us to deploy multiple timers with different countdown
periods. The FileOps external has an Object component to it, as
does the Graph external. There are also a number of useful external
components of Object type that ship with Omnis Studio, as well as
third party products available for purchase.
Most Object-type external components can be either the
superclass for an Object Class or directly set as the subtype for
an Object variable. The Timer object is best subclassed as an Object
Class first, since its $timer method must be overridden
for it to do anything when the timer countdown is complete. Of course,
this can also be done in our code, but modifying the code in an
Object Class is more straightforward. Subclassing an Object Class
from an external object also has the educational advantage that
we can then use the Interface Manager to explore all of its properties
and methods.
The modern Object DAMs also fall into this category. So we create
external objects to access SQL data sources in Omnis Studio.
Combined Use
In practice, some of these uses may be combined within a single
Object Class. For example, a Data Object might very well contain
a few functions that are specific to the type of data the object
is designed to manipulate, but that can also be invoked from outside
the Object to handle such values from any source.
Next Time
In the next article in this series, we will examine the four categories
of Object Instance uses mentioned above in more detail. I hope you
have found the discussion so far to be intriguing enough to come
back and read more... |