Object Classes and Instances:
Helper Objects
By David Swain
Polymath Business Systems
In the last article we explored
Function Objects. These are object instances designed to be called
implicitly from within expressions. We pass them one or more values
and they yield a return value based on the parameter(s) we supply.
As the name indicates, we use these just like we would use any other
function by calling their public methods implicitly from within
expressions.
Helper Objects are a bit different in their purpose and operation.
These are object instances that are called to perform some action
or series of actions affecting the properties and/or methods of
other instances, not usually to return a value. While we can certainly
call them implicitly if we wish, Helper Objects are more likely
to be invoked directly using the Do command.
But we must consider more factors in the design and implementation
of Helper Objects if we want our use of them to go smoothly. Perhaps
a brief analogy can help get this point of view across...
Little Known Facts ("You're a Good Man, Charlie Brown ",
Clark Gesner)
When we design physical structures and systems, we often need to
take into account a number of factors in choosing the right materials
or characteristics for specific components. For example, some materials
have great compressive strength while others have great tensile
strength. Few materials have both. We use materials with high compressive
strength (granite or concrete, for example) for components that
will be subjected to high compressive forces (foundations,
bridge footings, etc.). We use materials with high tensile
strength (fiber rope, steel beams or cable) for components that
will be subjected to high tensile stress (hoisting members
or suspension bridges). A suspension bridge made of concrete alone
will not even be able to bear its own weight, let alone vehicular
traffic! Other problems would occur if the pilings of a bridge were
made entirely of steel - but there would certainly be problems...
Other factors must be considered as well. The service environment
in which a component must perform its function is also an important
concern. Basic carbon steel in a marine environment won't last long
because it corrodes readily when exposed to that combination of
humidity and chemicals, but a different alloy might prove more corrosion
resistant or a special coating might provide protection from the
elements.
In electronic and electrical systems, we might need to be concerned
about impedance compatibility of current-bearing or signal-bearing
components in addition to physical and chemical attributes. And
the needs of micro-electronic components are even more interesting.
Alternatively, we might choose the materials for certain components
(fuses) for their ability to predictably fail.
The more sophisticated and sensitive the system, the more we must
be concerned about the composition of its components.
High temperature, low temperature, high humidity, exposure to various
chemicals - all these must at times be taken into account in the
design process in a physical system, in addition to the working
charactieristics of a component.
Gonna Build a Mountain ("Stop the World, I Want to Get Off
", Newley & Bricusse)
So why bring this up when discussing software? Virtual components
are no less sensitive to their environments than are physical ones
- there are just different factors for the designer to consider.
We build our components to perform certain kinds of jobs. The more
thought we put into both how and where these components will do
those jobs, the better will be our results.
The creators of Omnis Studio made certain decisions about how various
class types would be used. So we can do some things with Window
Classes and other things with Report or Menu Classes. We can't swap
a class of one type for a class of another type because they just
don't work the same. (Windows and Remotes Forms cannot be interchanged
- no matter how much we might want them to be - because they are
designed for very different environments.) But Object Classes were
intended to be a little more generalized in some ways. We can build
Helper Objects to assist instances of any class type -
and even to help instances of different class types to
work better together.
Object Instances are deployed as Object Variables, so we can put
them anywhere we can define a variable. But the outstanding characteristic
of a variable (besides its data type) is its scope. This is a determining
factor in our design decisions for a Helper Object.
There are two issues of scope to consider: The scope at which we
intend to deploy the instances of our Object and the scope of its
operation. These two factors often interact in the way they affect
our design choices. Of course, if we remain unaware of these considerations,
they only affect our effective use of our objects after
they have been built - and often in a detrimental way.
Here is a general rule of thumb for designing and using Helper
Objects: We must make sure we design our objects for the
environment in which they are to be used and then use
our objects in the environment for which they were designed.
Does this sound overly simplistic? It's not always so easy to follow
this advice - especially without a good plan. This is where many
developers get into trouble using Objects.
Fiddle About, Fiddle About... ("Tommy", the Who)
Helper Objects are also somewhat "instrusive" for Object
Oriented constructs. They are intended to stick their noses
into the business of other elements of an application, but only
for the sake of efficiency. Whether they do the jobs of surgeons
or the jobs of sanitary workers, they must still poke around in
and manipulate the properties and variables of other instances.
That is their job. Generally they only need to access public items
of other instances, but sometimes they need to deal with things
that are not strictly public in the OO sense, but that Omnis Studio
has left open to access from outside the instance (such as instance
variables of the instances the Objects are helping).
To do this job most effectively, each method of a Helper Object
Instance needs to know first where it is at and then where is the
target of its process. If we both design and use each Helper Objects
for a specific kind of environment, this is not much of an issue
after those decisions have been made. But if we try to build our
Helper Objects to be too general and then try to use them in a broad
range of environments and scopes, we can occasionally run into difficulties.
(The same goes for trying to use windows that are intended for use
as both standalone instances and nested three levels deep as subwindows.)
We then have to include additional resources (environment variables
and parameters) to orient the method so it understands where it
is working and on what. We will often find that Objects built too
generally are impossible to use in some environments.
But we don't want to build Helper Objects that are too
specific either. For example, we don't want to make Object Classes
that work with only one Window Class, just for the sake of using
Object Classes! Object Classes are sets of methods and variables
that we can reuse in many places, so we want them
to be generalized to a certain extent. We just need to be wary of
making them too general.
Perhaps an example can shed more light on this...
Where Am I Going? ("Sweet Charity", Cy Coleman)
Suppose that we want to build a Helper Object that manages the
status bar of Window Instances. It may contain methods for placing
or removing text in various panes of the status bar, for turning
a given status bar pane into a progress bar, etc. Depending on how
and where we intend to deploy instances of this Object Class, we
would address the target status bar in different ways in the code
of the methods it contains.
For example, if we design this object to always be deployed as
an instance (or even local) variable of a window
to manage the status bar of only the window instance that contains
it, we can address the status bar in the Object's methods using
$cwind.$statusbar. When this object is deployed, if deployed
according to design, its instance will always be housed
within the window instance whose status bar it is intended to manage,
so the $cwind notational shortcut points in precisely
the right direction.
Perhaps one of the methods in this object is used to place a message
in the first pane of the status bar. We might name this method $message1.
If we know that this will always address the first pane
of the status bar for the parent window of our Helper Object, the
method would contain this code:
Calculate $cwind.$statusbar.$panes.1.$text
as messagestring
The variable messagestring is a parameter of
Character data type. If we wish to clear the first pane
of the status bar, we can simply invoke this method and pass it
an empty string. Only one parameter is needed
here because our target can be narrowly and implicitly defined.
If we want to make this a bit more versatile, we could add a second
parameter (panenumber) that specifies which pane should
display the message string. This would most likely be a Short
integer variable. We would make this parameter optional by
assigning it a default (initial) value of 1. Having done
this, our method line would have to change to:
Calculate $cwind.$statusbar.$panes.[panenumber].$text
as messagestring
But maybe we don't want too many instances of this Object Class
taking up RAM, yet we intend to possibly have many window instances
simultaneously open on occasion. We might instead design this Helper
Object to be deployed as a task variable. $cwind
is no longer valid since the Object Instance now does not reside
inside a window. We then have an additional decision to make. The
question is, how will methods in this instance be invoked? Will
they be called from within the window whose status bar needs assistance?
Will they always apply only to the topmost window (which would make
sense if they are always invoked from event handling methods)? Will
we have to pass the name of the window instance or a reference to
it in order for the object to know which status pane it is to affect?
Depending on the answers to these questions, we might use either
$topwind, $iwindows.[windowname] or an Item
reference parameter variable to point the action in the proper
direction. Then we have to remember to use this Object consistently
and to pass in the proper parameter value(s) to indicate the target
window instance, if that is necessary. The more clever we try to
get with our deployment plans, the more parameters we need to supply
to make certain that the job is done correctly.
We Can Do It ("The Producers ", Mel Brooks)
Let's try another example. A technique that was very popular in
the Omnis 7 era was to open a window and perform some data entry
process as part of the initialization procedure (procedure
number zero) for that window. This was a fine thing to do in Omnis
7 because the window was constructed and drawn before this
procedure began execution (which caused other objections and interesting
workarounds from programmers - but that's another story). In Omnis
Studio, this is not a good thing to do with the $construct
method because the window instance is only supposed to be drawn
once that method completes its work. Performing a process that requires
any sort of data entry during the $construct method
can cause problems and will most likely fail in one way or another.
But we still want to create processes that work like this - and
there are good reasons to do so. For example, we might want to construct
a dialog window that gathers data values or that prompts the user
to make a selection as part of a larger process. How can we manage
to do such things in Omnis Studio?
One way is to use a Helper Object. This one needs to sit
outside the window instances (at least the ones it spawns) and manage
them from there. The task level of scope is again useful
for this purpose (although a strong argument can be made for using
an instance variable of the window that requires the dialogs - if
the Object only manages dialog windows). We might create
a method in such an object named $openandexecute, whose
job is to first open a window instance (our custom dialog window)
and then (after the window construction process has been completed)
invoke a method of that window (the data entry process). We may
need to send this method a number of parameters as well, but the
result is what we're after. Here is a brief summary of such a method:
First, our method needs a parameter for the name of the window
class that is to be instantiated. Let's call it windowname.
We can then use a method line like this to create the instance we
need:
Do $clib.$windows.[windowname].$open()
Returns winRef
This method instantiates the dialog window. The window's $construct
method executes completely. We capture a reference to the new window
instance in winRef (a local Item reference variable)
so that we can further manipulate it from this method.
Now that we have an instance of the dialog window, we need to execute
an appropriate public method of it (which will most likely contain
a modal data entry phase to pause method execution while the user
performs the task required by the dialog). We must also pass the
name of the method we wish to execute as a parameter of our $openandexecute
method. We will call this one methodname. The method must
be a public one for us to execute it from a method of our
Helper Object, so its name will begin with a dollar sign ($) character.
We can either include this in the parameter string value we pass
or exclude it and instead fix it permanently in the notation string
used to execute that method. Depending on our choice here, one of
the following two lines of code will execute the desired method:
Do winRef.[methodname]()
Do winRef.$[methodname]()
So if we have chosen the name dlog for the Object Variable
that will contain our Object Instance (our Dialog Helper Object)
at the task level of scope, if the name of our dialog window
class is testDialog, if the name of the method we wish
to execute once that window is instantiated is $enterdata
and if we have chosen to use the second technique given
above for executing that method, the method line in our existing
window instance that invokes the dialog would be:
Do dlog.$openandexecute('testDialog','enterdata')
We can further complicate this by adding parameters to contain
parameter strings to be sent to the $construct method and
the named method of the dialog instance. You get the idea!
All we need is a workable plan and a consistent execution of that
plan for a reasonable chance of success.
Make Our Garden Grow ("Candide", Leonard Bernstein)
This all may seem a bit complicated at first, but this is the Omnis
world in which we now live. Its rules govern how we make our methods
work - and no amount of argument about how it should work
some other way or it used to work some other way will change
that. The lesson we must learn is to work with the flow
of Omnis Studio and not against it. Learn the ways of Objects in
Omnis Studio and have a long and productive development career!
The variety of uses we can find for Helper Objects is vast, so
we can't possibly deal with all of them here. They are characterized
by performing actions on other items within an application
rather than returning values. Generally, the majority of
variables they contain are parameter and local
variables of their methods. There are few uses for instance variables
within these Objects other than as constants or environment variables
determined when an Object Instance is first constructed.
The Object Instances that rely heavily on their own instance variables
are what I call Data Objects. We will discuss those when we return
to the discussion of Object types in a couple of months.
So Long, Farewell ("The Sound of Music", Rodgers and
Hammerstein)
Omnis Studio version 4.1 has now shipped and there are plenty of
new and improved features to explore! We will take a break from
Object Instances for a couple of months to see what's new with Omnis
Studio 4.1. There are a number of useful new items not even mentioned
in the What's New document - and many more just begging for more
complete explanations and examples!
|