September 94 - Implementing Inheritance In Scripts
Implementing Inheritance In Scripts
PAUL G. SMITH
"Programming for Flexibility: The Open Scripting Architecture" in develop Issue 18
showed you how to use scripts to increase your program's flexibility. This article
builds on that one and explains how to implement an inheritance scheme in your
application that will enable your AppleScript scripts to share handlers and properties
and to support global variables. You'll also learn a way to support inheritance in other
OSA languages with just a little extra work.
In Issue 18 ofdevelop , I showed you how to attach scripts to application-domain
objects and how to delegate the handling of Apple events to those scripts. I left you with
a challenge: to figure out how to support global variables and to enable scripts to share
subroutines and handlers. To meet this challenge you need to implement inheritance.
The AppleScript 1.1 API gives you all the necessary calls to implement inheritance in
embedded AppleScript scripts, but not all are documented yet inInside Macintosh . This
article documents the calls you need and describes an inheritance scheme that relies on
them.
In a nutshell, here's the scheme :
1. Decide what kind of script inheritance hierarchy to use.
2. Link your scripts together in an inheritance chain.
3. Create a shared handlers script to define subroutines and Apple event
handlers that are shared among all scripts. Make this script the parent of all
other scripts in your program, in effect putting it at the end of the inheritance
chain.
4. Create a global variables script and add this to the start of the inheritance
chain so that it's the first script to receive incoming messages. Save this
script to disk when the program exits and reload it when the program
restarts, so that variables persist.
You can use much the same scheme to implement inheritance in other Open Scripting
Architecture (OSA) languages, but more work is required to link scripts together in
an inheritance chain, and you must forgo the luxury of sharing global variables
between scripts. At the end of this article, the section "Inheritance in Other OSA
Languages" describes the extra work your program must do.
The sample program SimpliFace2 on this issue's CD demonstrates the inheritance
mechanisms discussed here. SimpliFace2 is an extension of SimpliFace, the basic
interface builder used to illustrate the article in Issue 18. The SimpliFace2 sample
code has a compile-time flag qUseOSAinheritance, defined in the header file
SimpliFace2Common.h. If this flag is undefined, the program uses the
AppleScript-specific inheritance scheme described in the bulk of this article. If the
flag is defined, SimpliFace2 uses a general-purpose scheme that involves the extra
work outlined in the section on inheritance in other OSA languages.
CHOOSE A SCRIPT INHERITANCE HIERARCHY
The first thing to do is to decide what kind of script inheritance hierarchy to use.
Youcan use a runtime containment hierarchy (like that used by HyperCard and
FaceSpanTM), a class hierarchy (like that used by AgentBuilder), or some hybrid of
the two, as demonstrated by SimpliFace2. Let's look at each of these hierarchy types in
turn.
FaceSpan (formerly Frontmost) is the interface builder bundled with the
AppleScript 1.1 Software Development Toolkit. Perhaps the best known OSA "client,
FaceSpan was developed by Lee Buck (of "WindowScript" fame) of Software Designs
Unlimited, Inc. AgentBuilder, from commstalk hq and Full Moon Software Inc., is a
framework for the creation of communications and information-processing agents that
uses embedded OSA scripts to customize agent behavior. *
Figure 1 shows a runtime containment hierarchy. In this kind of hierarchy, objects
inherit behavior from their containers at run time. In the object containment
hierarchy used by HyperCard, for example, the scripts of buttons and fields within
cards are at the bottom of the hierarchy. Above them are the scripts of the cards that
contain the buttons and fields, and above each card script is the script of the
background that contains the card. Above each background script, in turn, is the script
of the stack that contains all the backgrounds. The handlers in each container's script
are shared by the scripts of all the objects it contains.
Figure 2 shows a class hierarchy. In this kind of hierarchy, objects inherit behavior
from their parent classes. For instance, the behavior of AgentBuilder objects is
defined in an "ancestor" object of each class, which is the parent of all instances of that
class. This permits the standard scripted behavior of object classes to be overridden in
derived class instances.
Figure 1. A runtime containment hierarchy
Figure 2. A class hierarchy
Figure 3 shows the hybrid script inheritance hierarchy used in SimpliFace2. In
SimpliFace2, the scripts of user-interface objects -- such as windows, labels, and
buttons -- are organized so that they inherit behavior from the runtime containment
hierarchy. However, the script of the application object isn't included in the
inheritance chain for the script of any user-interface object, and the shared handlers
script becomes the ultimate parent of all other scripts. I chose to use this hybrid
hierarchy in order to demonstrate a wider range of techniques, not for any reason
intrinsic to the program design.
The kind of script inheritance hierarchy to use depends on the nature of the messages
being handled in your program. Using a class hierarchy is most appropriate if the
messages are Apple events defined in the program's 'aete' resource. If the incoming
messages are primarily user-defined subroutines being handled inside scripts, using
a runtime containment hierarchy is probably more natural for the scripter.
Figure 3. The hybrid script inheritance hierarchy used in SimpliFace2
Another way to look at this choice is that if you want to enable users to customize your
program's capabilities by attaching scripts to application-domain objects, using a
runtime containment hierarchy isn't always the best idea. Because different
application-domain objects handle the same Apple event message in different ways (in
other words, the semantic meaning of the message differs depending on what object it's
directed at), unwanted side effects could result from an object's handling an Apple
event message intended for a different level in the containment hierarchy. Using a
class hierarchy ensures that messages will be dealt with only by objects of the class
that understands them.
Once you've chosen the type of script inheritance hierarchy most appropriate for
your program, you can link scripts together in an inheritance chain.
LINK SCRIPTS IN AN INHERITANCE CHAIN
Linking scripts together in an AppleScript inheritance chain is as simple as setting
their parent properties. Before I tell you how to do that, though, let's review a few
facts about script objects and inheritance. As mentioned in the Issue 18 article, a
script context (a script compiled using the AppleScript OSA component) is equivalent
to a script object in the AppleScript language, so everything I say here about script
objects applies to script contexts as well.
ABOUT APPLESCRIPT SCRIPT OBJECTS AND INHERITANCE CHAINS
Script objects can contain global variables, properties, and handlers for Apple event
messages and subroutine calls. A script object can have as its parent property an
object specifier or another script object. Thus, one script object can become the
parent of another, and the child script object can inherit properties and handlers from
the parent script object. Parent and child script objects are linked together in an
inheritance chain; this is the path from child to parent to grandparent and so on in an
inheritance hierarchy, as illustrated in Figure 4.
For (a lot) more on object specifiers, see "Apple Event Objects and You" by
Richard Clark in develop Issue 10.*
Figure 4. A script inheritance chain
An incoming Apple event message is received by the child script object at the start of
the inheritance chain. If AppleScript can't resolve a reference to a handler or variable
name within this script object, it searches through the entire inheritance chain to find
it. The handler or variable is resolved wherever it's found in the inheritance chain.
When a handler continues a message (that is, passes the message to its parent),
AppleScript starts searching in its parent script object. Messages that target objects
outside the program's domain, or that aren't handled anywhere in the script
inheritance chain (such as Apple events defined in the program's 'aete' resource,
which are handled in the program code instead), or that are continued out of the
inheritance chain, are redispatched as Apple events.
SETTING A SCRIPT'S PARENT PROPERTY
Now that you understand the dynamics of script inheritance, I'll show you how to set a
script's parent property and thus link it to an inheritance chain. In the AppleScript
language, you simply say what you'd like the parent set to, as illustrated here:
script mom
on getName()
return "Fenella
end getName
end script
script toddler
property parent : mom
on getName()
set myMom to continue getName()
return "Bart, son of " & myMom
end getName
end script
getName() of toddler --> returns "Bart, son of Fenella
To set the parent of a script context from a programming language, you can use the
AppleScript routine OSASetProperty. This general-purpose routine (defined in the
header file ASDebugging.h, which was added with the AppleScript 1.1 API) accesses
either a predefined property or a user- defined variable, depending on the AEDesc
passed to it. To access a predefined property -- the parent property -- you create a
descriptor of typeProperty (not typeType), specifying the property ID as the data.
The parameters to the call are (1) the scripting component (probably the AppleScript
component), (2) a mode flag (we use the null mode, indicating no special action should
be taken; alternatively, we could instruct AppleScript to replace the property only if
it already exists), (3) the script context ID that's to be changed, (4) the AEDesc, and
(5) the value you're setting the property to, in our case the new parent. The OSA
routine OSAGetProperty performs the converse function: you can use it to inspect the
values of properties and variables.
Here's a fragment from SimpliFace2 that sets the parent of a script by calling
OSASetProperty:
OSAError err = noErr;
AEDesc nameDesc;
DescType thePropCode = pASParent;
err = AECreateDesc(typeProperty, (Ptr)&thePropCode,
sizeof(thePropCode), &nameDesc);
err = OSASetProperty(scriptingComponent, kOSAModeNull, contextID,
&nameDesc, newParentID);
AEDisposeDesc(&nameDesc);
}
The structure of the inheritance chain is static; each parent link needs to be set up
only once, as long as no scripts are replaced. The only exception to this is that the
parent property of the global variables script used in SimpliFace2 needs to be set
every time an incoming Apple event message is handled, as I'll explain later. Whenever
a script in the chain is replaced by a new one, the script's OSAID will change and you'll
need to set the parent property in the new script and in its children again.
STRIPPING COPIED PARENT SCRIPTS
By setting the parent properties of scripts and thus linking them in inheritance
chains, your program limits unnecessary duplication of script objects. Still, when
AppleScript sets the parent of a script, it stores a copy of the script's parent (and of
the parent's parent, and so on) with the original script. This is the basis of the trick
that allows SimpliFace to simulate sharing scripts between objects: every script
carries with it a copy of all the scripts it shares. But this is wasteful -- it means
that, for instance, each button script for a window contains a copy of the window's
script, when only one copy is necessary. Because your program is directly controlling
script inheritance chains, you'll want to block this behavior when it loads and stores
scripts. You can do it by specifying the kOSAModeDontStoreParent flag when you call
OSAStore and recreating the inheritance chain when the scripts are reloaded.
Listing 1 shows the routine used to set the script property of an object in
SimpliFace2. Note how it's changed from the routine used in SimpliFace: it now strips
the copied parent scripts from the incoming script so that SimpliFace2 can manage the
inheritance chain itself.
Listing 1. TScriptable Object::SetProperty
OSErr TScriptableObject::SetProperty (DescType propertyID,
const AEDesc *theData)
OSAError err = errAEEventNotHandled;
switch (propertyID) {
case pScript:
OSAID theValueID = kOSANullScript;
if (theData->descriptorType == typeChar
|| theData->descriptorType == typeIntlText)
err = OSACompile(gScriptingComponent, theData,
kOSAModeCompileIntoContext, &theValueID);
else { // If it's not text, we assume script is compiled.
err = OSALoad(gScriptingComponent, theData, kOSAModeNull,
&theValueID);
// The following new section strips any existing parent
// script.
AEDesc newData;
err = OSAStore(gScriptingComponent, theValueID,
typeOSAGenericStorage,
kOSAModeDontStoreParent,
kOSAModeDontStoreParent, &newData);
OSADispose(gScriptingComponent, theValueID);
theValueID = kOSANullScript;
err = (OSErr)OSALoad(gScriptingComponent,
&newData,kOSAModeNull, &theValueID);
AEDisposeDesc(&newData);
}
}
}