|
Understanding and Using ValueModels
By Bobby Woolf
Introduction
In Objectworks 4.1, ParcPlace introduced the Value Model framework.
Back then, it only consisted of two concrete classes. The Objectworks
code didn't use this framework very much, so neither did most application
developers. However, this framework was greatly enhanced to help
form the foundation of ParcPlace's next release, VisualWorks 1.0.
This version of Smalltalk expanded the framework to contain numerous
concrete subclasses and associated classes.
This document will answer these questions:
- What is the ValueModel framework?
- Why was this framework developed?
- How should developers use this framework?
- How specifically should developers use the framework's concrete
classes?
VisualWorks uses this framework to convert users' painted views into
application models containing executable code. The framework is used
extensively by the generated code in application models and the code
developers write to enhance their application models.
However, the framework does more than just enable VisualWorks to
automatically generate application model code. It significantly
enhances the dependency framework and helps developers separate
application models and domain models into distinct objects. In fact,
although VisualWorks' code generators use ValueModels exclusively
in ApplicationModel s , they may be even more useful when employed
in domain models.
This paper contains guidelines explaining what ValueModel s are,
why they were developed, and how to use them. You, the reader, are
expected to be familiar with ParcPlace Smalltalk (either Objectworks
4.x or VisualWorks) as well as basic Smalltalk syntax and object-oriented
practices. You should have a basic understanding of the dependency
framework whereby an object notifies its dependents of a change
by sending itself changed... , which causes each dependent to receive
update:... . You should have used the UI Painter facility in VisualWorks
to create your own subclass of ApplicationModel and design a view
for it.
In general, though, intimate knowledge of Smalltalk is not required
to understand these guidelines. Although they document the Value
Model framework in VisualWorks, the concepts behind the framework
that motivate its use are not VisualWorks specific or even Smalltalk
specific (although they may be specific to object-oriented programming!).
Thus even though these guidelines are designed primarily for VisualWorks
programmers, other developers using object-oriented techniques can
benefit from them as well.
What is a ValueModel?
"Value Model" is the name of a framework in VisualWorks.
It is implemented by the ValueModel hierarchy, but also has some
related classes outside that hierarchy like SelectionInList and
DependencyTransformer . ValueModel itself is an abstract class;
you actually use instances of ValueModel 's concrete subclasses.
A ValueModel has two main characteristics:
- Its aspect is always value . This means that its getter message,
setter message, and update aspect are value , value: , and #value
respectively.
- It informs its dependents whenever its value changes. It has
a standard mechanism for registering interest in the value, and
those objects that have done so will be notified in a standard
way.
This provides a simple, generic interface between any object (a
value) and a value-based object (such as a visual widget that displays
a single value). The value and the value-based object do not have
to be customized for each other; the ValueModel does this by connecting
the two and translating the interactions between them as necessary.
Thus the value-based object neither knows nor cares where the value
comes from or how to access it. The object is able to simply send
value to its ValueModel , which in turn does whatever is necessary
to obtain the value, then returns it.
If the value changes, it notifies the widget. This is the case
even when multiple widgets share the same value; when one widget
changes the value, it does not have to perform any notification
because the ValueModel does so. In this way, the widget simply uses
the value as it sees necessary and does not have to worry about
what the consequences might be to anyone else who might also be
using the value.
Here are some guidelines that describe what ValueModel s are and
what they do:
Use a ValueModel to generalize an object's aspect
Problem
How can one object retrieve and change a collaborator object in
a uniform way, regardless of what larger object the collaborator
may be a part of?
Context
The object will treat its collaborator as a single value. It will
need to be able to retrieve this value, and change its setting by
storing a new value. Since the value may change, the object will
need to be notified when the value changes.
The object will need to send messages to retrieve and set the value.
The object's design can be simplified if these messages are always
the same. It will also need to listen for notification that the
value has changed to a new object. Again, if this notification is
always the same, the object's design can be simplified.
The value may be stored in several different ways: It may be a
stand-alone object. It may be a part of a larger object. It might
not be an object at all, but rather one that can be generated from
others whenever it is needed. If these specifics of how the value
is stored can be hidden from the object using it, that object's
design can be simplified.
Behavior will be needed to allow a value to be accessed using standard
messages. More will be needed to access it, depending on how it
is stored. This behavior should be implemented in a way that is
as reusable as possible. Then the code that handles these issues
for one value can be used with all values that have similar circumstances.
Solution
Use a ValueModel to store the value. ValueModel is an abstract class,
so you'll actually use an instance of one of ValueModel 's concrete
subclass. Which subclass you'll use will depend on a couple of factors.
How is the value stored in the system? How does it needs to be retrieved
from the system? What conversion needs to be performed on the object
during the retrieval process? (See "3. Types of ValueModels".)
A ValueModel has the aspect of value . This means that to retrieve
the value, the getter message is value ; to change the value, the
setter message is value: ; and to listen for updates when the value
changes, the update aspect is #value .
Once the ValueModel is established on the value, you don't need
to know where it comes from, what its real name or aspect is, or
what other objects might be using it. The ValueModel takes care
of these details. You automatically know how to retrieve it and
change it using the standard value aspect. In this way you always
know how to use the value and don't need to be concerned about the
details.
Example 1
Let's say you want to implement a time widget that displays a clock
face on a view. (Maybe you implement two, analog and digital.) Its
state will be based on an instance of Time .
Where might this instance of Time come from? Someone might create
one in a workspace and send it displayAsClock . It might be one
half of a DateTime object, or an aspect of a LogEvent object. It
might sit in a particular place in memory, the result of a primitive
that constantly reads the system clock and replaces the Time instance
whenever the clock changes. As you can see, you don't know where
it might be coming from.
And you shouldn't know where it's coming from. Even if you're implementing
the clock view for one particular model, you should generalize your
implementation to fit any Time instance that comes along.
So design it to access its Time object via a ValueModel . That
means that it uses value to get the Time from its view's model,
value: to set it, and re-performs the get when it receives a #value
change notification.
Which concrete subclass of ValueModel you use will depend, case
by case, on how the Time is stored in the system. With the appropriate
kind of ValueModel , your clock widget will work with any model
that contains a Time , including a stand-alone instance of Time
itself.
Example 2
Most of the widgets that use ValueModel s are visual widgets. For
an example that is a model widget, see SelectionInList
Whereas a ValueModel has one aspect, value , SelectionInList has
three: list , selectionIndex , and selection . The first two are
implemented "physically" through the instance variables
listHolder and selectionIndexHolder ; the third is implemented "logically"
in terms of the first two. The two instance variables are, you guessed
it, ValueModel s (actually ValueHolder s ; see "3.1 Use a ValueHolder
to hold a stand-alone object").
1.2 Use a ValueModel to share a value
Problem
How can two objects share a common value in such a way that both
can access and change the common value, and whenever one changes
the common object the other will be notified automatically?
Context
The change/update mechanism in VisualWorks enables an object to
notify its dependents when its internal state changes. But that
mechanism does not work when the object is replaced with an entirely
new object; all of the dependents are still attached to the old
object. The dependents and the old object are unaware that the old
object has been replaced with the new one.
The behavior that is performing the swap could use the message
become: to change all pointers to the old object to point to the
new one. However, become: is inefficient, makes code difficult to
maintain, can cause unwanted side effects, and doesn't notify dependents.
It will confuse objects that wanted to point to the old object even
if it were replaced with a new one, and the dependents will not
be notified that the replacement has been made.
For a set of objects that are sharing a value to all automatically
share a replacement value, their pointers require a level of indirection.
Instead of pointing directly to the shared object, they should point
to an object that will not be replaced, which in turn will point
to the shared value. This intermediary object will be a container
that holds the value being shared.
Because the objects sharing the value will have to access it through
the container, that container will know when the value has been
replaced. Thus the container can notify the dependents whenever
the value is replaced.
Solution
When two objects need to share a common object, they should place
that object in a ValueModel and share that ValueModel . They can
then register their interests on the value and the ValueModel will
notify them when the value changes (see "2.1 Use onChangeSend:to:
to register interests on a value"). To manipulate the value,
they will go through the ValueModel (see "1.1 Use a ValueModel
to generalize an object's aspect"), allowing it to monitor
their actions. As necessary, it will execute overhead behavior like
translating the value or notifying dependents of a change.
Example
Let's say two application models need to use the same value in a
domain model. Either application model might change the value, in
which case the other needs to be informed so that it can update
itself accordingly.
A simple way to accomplish this is for the domain model to store
the value in a ValueModel . Then each application model can latch
on to the ValueModel and access the value by sending the model value
. Each one can change the value by sending the model value: . Of
course, when one changes the value, the other needs to know about
it, so both will register their interest with the ValueModel (see
"2.1 Use onChangeSend:to: to register interests on a value").
This way, when the value changes, the application models will be
notified. In fact, the object changing the value may not be one
of the application models. The domain model may decide to change
its internal state, and in doing so, change the value. Even in this
case, all of the value's dependents (who registered themselves as
such), including the two application models, will be notified.
2. How to use ValueModel s
A ValueModel is a powerful mechanism that will abstract an object's
aspect and define its dependents. However, given that ValueModel
s provide this behavior, how should programmers design them into
their code to implement useful system functionality?
ValueModel s also introduce a layer of indirection that can quickly
complicate the model's use of its aspects and obscure the interface
protocol for the collaborators that use those aspects. When a programmer
first starts to use ValueModel s , he can easily become confused
about when he needs to send value or value: to an object. Because
of this confusion, he will have difficulty getting otherwise simple
code to work. Once his code does work, it often contains unneeded
senders of value and value: ; these can lead to more subtle problems.
This section contains guidelines and tips for how to write code
that uses ValueModel s . A programmer does not have to follow these
guidelines to write code successfully, but they will often make
the code simpler and better encapsulated. This will be an especially
big help to someone trying to learn how to use ValueModel s .
Here are some guidelines that describe how to use ValueModel s
:
2.1 Use onChangeSend:to: to register interests on a value
Problem
How can an object that is using the value contained in a ValueModel
be notified whenever the value changes?
Context
When multiple objects share a value, they should share a single
ValueModel wrapped around that value (see "1.2 Use a ValueModel
to share a value"). When the ValueModel changes the value,
it will notify its dependents.
To be notified, each dependent must register its interest in this
value (hence making itself a dependent). This will make it a logical
dependent of the value (which is implemented as making it a physical
dependent of the ValueModel ).
Since the only update aspect a ValueModel ever sends is #value
, this is the only one the dependents must listen for.
It is insufficient for a dependent to simply listen for notification
of a change. When it receives such notification, it must perform
actions to respond to the change. To encapsulate this series of
actions and give it a name, they should be collected together into
a method (which in turn may use other methods). By collecting this
series of actions into a method, the series can be reused whenever
it is needed, even if the need is not caused by a change notification.
For example, a newly created object might perform these actions
to initialize itself, then re-perform them whenever it receives
a change notification.
When an object's design dictates that it be a dependent of a value,
the code to implement that assumption should be encapsulated inside
the object.
When an object is no longer being used, it should release its dependencies
so that it will no longer receive notification of changes.
Solution
To register your interest in a ValueModel 's value, send the ValueModel
aValueModel onChangeSend: aSelector to: aDependent
where aSelector is the name of the method you want run when the
value changes and aDependent is the object that contains the method
(usually yourself).
The onChangeSend:to : messages should be sent by the initialize
method of the dependent object; this means that the to: parameter
will be self. By establishing the dependency during initialization,
this ensures that it will be established only once, and that it
will be established before the object is used.
The dependent object should also implement release to release its
dependencies by sending
aValueModel retractInterestsFor: aDependent
to the ValueModel . In this way, for each sender of onChangeSend:to
: in initialize, there should be a corresponding sender of retractInterestsFor
: in release.
(Note: In practice, sending retractInterestsFor
: when the dependent is released is not always necessary. When the
ValueModel 's container is released and garbage collected at the
same time as the dependent is, whether or not their dependency is
disconnected is irrelevant. However, in many cases, the dependent
is released while the ValueModel 's container remains in use. In
such cases, to keep the obsolete dependent from receiving updates
from the ValueModel , it should release its dependency as part of
releasing itself. Thus it's safest to always release the dependency,
even though this sometimes is not necessary.)
Examples
Example 1
An application model needs to know when the value in the domain
model changes. Luckily the domain model stores the value in a ValueModel
so that the application model can easily register its interest in
the value and receive notification when the value changes. The application
model has a method, domainValueChanged , that it wants to run whenever
the value in the domain model changes.
Here's the code in the application model to do this:
initialize
. . .
self domainModel sharedAspectHolder
onChangeSend: #domainValueChanged
to: self.
. . .
domainValueChanged
"The domain model changed; update the app model"
self sharedAspect: self domainModel sharedAspect
release
. . .
self domainModel sharedAspectHolder
retractInterestsFor: self.
. . .
(In the above examples: domainModel is a method that returns the
application model's domain model; sharedAspectHolder returns the
ValueModel holding the value of interest (see "2.3 Encapsulate
senders of value and value:"); and domainValueChanged performs
the steps in the application model necessary when the value changes.
In real code, these methods would have more descriptive names.)
Example 2
I have a list of Thing s being displayed in my view, and I want
selectedThingChanged run whenever the selection in the list changes.
Here's the code (see also "3.10 Use a SelectionInList to hold
a list and its selection"):
- initialize: "self thingsSelectionInList selectionIndexHolder
onChangeSend: #selectedThingChanged to: self"
selectedThingChanged : "self selectedThing: self thingsSelectionInList
selection"
release: "self thingsSelectionInList selectionIndexHolder
retractInterestsFor: self".
2.2 Use ValueModel chains instead of onChangeSend:to:
Problem
What if the update that is performed when a change occurs is so
simple that a separate <something>Changed method seems unnecessary?
Context
If an object is to understand a particular message, it must implement
or inherit the corresponding method. Each such method adds to the
object's bulk and the amount of code a developer must learn to maintain
the object. Thus an object should not contain methods it does not
need.
Each time a dependent registers its interest on a value using onChangeSend:to
: (see "2.1 Use onChangeSend:to: to register interests on a
value"), it must specify a <something>Changed message
in itself to be sent. Thus the dependent must implement or inherit
a method for this message.
Complex methods are necessary; they are how objects implement their
behavior. But simple <something>Changed methods that do nothing
more than re-fetch the value that changed do not significantly enhance
the overall behavior provided by the object.
An object designed to automatically re-fetch a value whenever it
changes would encapsulate this functionality. It would automatically
initialize its value in the dependent object, then update it when
the value in the parent changes. This behavior would be easy to
reuse, and its role would be easily recognizable, simplifying maintenance.
Solution
Rather than implementing an extremely simple <something>Changed
method in the dependent to be sent by onChangeSend:to : , eliminate
that extra step by using a ValueModel instead. This will connect
the parent and dependent models using two ValueModel s . The parent
object will contain the first ValueModel ; that ValueModel will
be the subject of the second, which will in turn be contained by
the dependent object. Whenever the value in the parent changes,
the first notifies its dependents, which causes the second to update
its value in the dependent.
Example 1
A domain model has an aspect address . The application model needs
to hold this address so that it can display it in its view. The
domain model should store its address in a ValueHolder (see "3.1
Use a ValueHolder to hold a stand-alone object") so that the
application model can register its interest on the value by sending
the ValueHolder onChangeSend: #addressChanged to: self (see "1.2
Use a ValueModel to share a value"). addressChanged would simply
read the new value from the ValueHolder and store it into the application
model: self address: self domainModel address.
However, the addressChanged method is so simple that it's not even
necessary. Instead, make a variable in the application model that
stores the value in an AspectAdapter (see "3.2 Use an AspectAdapter
to hold an aspect of an object"). The AspectAdapter 's subject
channel is the address ValueModel in the domain model and its aspect
is value. This way, whenever the address value in the domain model
changes, its ValueHolder will issue an update. This will trigger
the AspectAdapter to issue an update, which will cause its dependents
(such as the field subview that displays the address) to update.
All of this is done as a chain of events without you needing to
write any further code (such as addressChanged ).
(Note: You could almost have the instance variable
in the domain model and the one in the application model contain
the same ValueHolder . Unfortunately, this typically doesn't work
because VisualWorks feels compelled to insert a TypeConverter (see
"3.8 Use a TypeConverter to convert a value between types")
between the domain model's ValueHolder and the application model.
It does this because the ValueHolder can hold any object but the
application model requires a specific type, such as a String or
a Number . Thus the TypeConverter is needed to guard against the
ValueHolder containing nil, which will be converted to the empty
string or zero. Having both models share the same ValueModel might
seem to be the simplest solution, since it uses only one ValueModel
instead of two. However, VisualWorks will insert one anyway, so
you get two ValueModel s even if you only specify one. Since an
AspectAdaptor is a little more efficient than a TypeConverter ,
you might as well specify which type of ValueModel to use.)
Example 2
The application model contains a list and a field that should display
the current selection in the list. You could use a SelectionInList
and send its selectionIndexHolder onChangeSend: #selectionChanged
to: self , where selectionChanged reads the selection and stores
that in the field's ValueModel (typically a ValueHolder ).
There is, however, a simpler way that avoids implementing selectionChanged
. Make the field's ValueModel a PluggableAdaptor like this:
field := (PluggableAdaptor on: listSelectionInList)
getBlock: [ :m | m selection]
putBlock: [ :m :v | m selection: v]
updateBlock: [ :m :a :p | a == #selectionIndex].
This way, whenever the selection changes, the PluggableAdaptor
will catch the update and update the field with the new value (see
"3.7 Use a PluggableAdaptor to hold some part of an object").
(Note: The field should really just use an AspectAdaptor
whose aspect is selection (see "3.2 Use an AspectAdapter to
hold an aspect of an object"). However, when a SelectionInList
's selection changes, it issues the update aspect #selectionIndex
, not #selection . Since an AspectAdaptor 's update aspect must
be the same as its get selector, you have to use a PluggableAdaptor
because it allows you to specify them separately. This would no
longer be necessary if SelectionInList were fixed to issue both
#selection and #selectionIndex update aspects whenever the selection/selectionIndex
changed.)
Example 3
Use AspectAdaptor s (see "3.2 Use an AspectAdaptor to hold
an aspect of an object") on a "selection channel"
to allow the selection of an object and display its aspects. In
this technique, a ValueModel is set-up to hold the current selection.
If this selection is made via a SelectionInList , use the technique
described above to attach the ValueModel to the SelectionInList
. This ValueModel that holds the selection is called a selection
channel. With the selection channel established, attach AspectAdaptor
s (or other ValueModel s ) to it to display the object's aspects
(or other values).
With this arrangement, whenever the selection changes, the selection
channel's value changes, which triggers the AspectAdaptor s . They
in turn re-read their values and update their dependents, which
redisplay or otherwise update themselves.
This selection channel technique is well documented in [Par93].
2.3 Encapsulate senders of value and value:
Problem
When using ValueModel s , how does the programmer know when to use
value and value: ?
Context
To use a variable's value, a programmer must know what type of object
the value is. This way he knows what protocols the value supports
and thus what messages it will understand.
When a message returns a value (besides self ), the message name
should describe or suggest the value returned.
When using ValueModel s , the programmer must know which messages
return the ValueModel s and which return the values themselves.
Usually the sender will be interested in the value, not the ValueModel
that contains it.
An object's accessing protocol should simply provide its aspects
while hiding how those aspects are implemented. This will better
encapsulate the object and make it simpler to maintain.
Solution
Implement separate messages for accessing an object's aspects (values)
verses accessing the ValueModel s that contain those aspects.
If a model has an aspect called aspect that is being stored in
a ValueModel that will be held in an instance variable, name the
instance variable aspectHolder . Initialize the instance variable
in the initialize method to be the necessary kind of ValueModel
. Create a getter for it named after the instance variable, aspectHolder
, but do not implement a setter for it (see "2.4 Ensure that
all objects sharing a value use the same ValueModel"). Also
implement a pair of getter/setter methods for the value, aspect
and aspect :, that use the aspectHolder method.
Use the message aspectHolder when you need the ValueModel , like
to send it onChangeSend:to : (see "2.1 Use onChangeSend:to:
to register interests on a value"). Otherwise, use aspect and
aspect: to get and set the value; they encapsulate the object that
implements them, simplify its accessing protocol, and hide exactly
how the value is stored.
Example 1
A model has the aspect address which the model stores in an instance
variable. You need to be able to update other values when the address
changes, so you'll store the address in a ValueModel and the other
values will register their interests on it using onChangeSend:to
: . You want to avoid using senders of value and value: in your
code.
Name the instance variable addressHolder and initialize it in initialize
to be an appropriate kind of ValueModel . Implement addressHolder
to return the instance variable's value, but don't implement addressHolder
: . Implement address as " ^ self addressHolder value "
and address: as " self addressHolder value: newAddress ".
Now, to get and set the address, use the messages address and address:
. To be notified when the address changes, send onChangeSend:to
: to the result of the message addressHolder .
Example 2
The model in Example 1 is an ApplicationModel ; the aspect address
is being set via the Properties Tool or Multi Tool.
Specify the aspect as addressHolder (to get VisualWorks to give
the instance variable the name you want). When you install and define
the model, it will create the instance variable addressHolder and
implement the method addressHolder for you (and not implement addressHolder
: , which is fine). Now implement address and address: as directed
above.
Example 3
If the instance variable is to hold a SelectionInList (not a ValueModel
), don't call it <list>Holder because that's misleading. A
variable named <aspect>Holder should understand the value
aspect, but a SelectionInList will not. Instead, name the variable
<aspect>SelectionInList or <aspect>SIL , which indicates
that it understands the aspects list and selection .
2.4 Ensure that all objects sharing a value use the same
ValueModel
Problem
How can multiple objects that need to share the same value be assured
of sharing a single ValueModel ?
Context
When two objects need to share the same object, they should share
a single ValueModel wrapped around that object (see "1.2 Use
a ValueModel to share a value").
The shared ValueModel will have to be stored in a commonly accessible
place; usually, one of the objects will store the ValueModel and
make it accessible to the others.
If the ValueModel stored in this common place is shared by a couple
of objects, then changed to contain a different ValueModel instance,
not all of the objects will be sharing the same ValueModel .
To make sure they are all sharing the same ValueModel instance,
the ValueModel should be set once and then never changed.
Solution
When an instance variable holds a ValueModel , create a getter method
for it but not a setter method, and initialize it in the initialize
method. This way, it gets set once and will never get reset.
This guideline will be sufficient for typical uses. There are some
fairly advanced uses of ValueModel s , however, where a method to
set the ValueModel externally is necessary.
Example
See the examples in "2.3 Encapsulate senders of value and value:".
2.5 Keep a ValueModel's value's type consistent
Problem
When an object gets a value from a ValueModel , how does it know
the type of object it will receive?
Context
ValueModel is a container object. The object it contains, its value,
can be any type of Object . ValueModel is implemented to work with
any type of Object as its value.
Collaborators using an object interact with it through one or more
behavior protocols. Thus the object must be able to perform the
protocols. In implementation terms, this means that the object must
understand the messages being sent to it, which are the messages
in those protocols.
An object's type is defined by the protocols it is able to perform.
The more specialized an object's behavior is, the more specialized
some of its protocols are. Collaborators will interact with specialized
objects using specialized protocols that most objects don't perform.
Although a ValueModel can hold any type of object as its value,
the collaborators using that value may use a protocol that not all
objects perform. Thus the value in a ValueModel cannot be just any
type of object, it must be of a type that supports the protocol
that its collaborators will use. If its collaborators use several
protocols, the value must be of a type that supports all of them.
Solution
When defining a variable's type as ValueModel , also specify the
ValueModel 's value's type. Declare and initialize the ValueModel
with a valid value. When changing the ValueModel 's value, ensure
that the new value is of the specified type.
When implementing code, you must make sure that the object that
is used as an argument to value: is of the specified type. This
way, when other objects retrieve the value, they can assume its
type is correct and that it will support the protocols they will
use.
Example
Let's say a Person domain object contains an age aspect that must
be a non-negative integer. age is stored in a ValueHolder to simplify
its use.
These code segments represent valid uses of age that will keep
its type, Integer , consistent:
- definition comment in Person : "<ValueModel on: Integer>"
- Person>>initialize: "ageHolder := ValueHolder on:
0" ( 0 is an Integer )
- Person>>age: : "newAge >= 0 ifTrue: [self ageHolder
value: newAge]" (enforces that age is positive)
- "self age: 21" ( 21 is an Integer )
- "self age + 1" ( Integer s understand + )
- "self age: self age // 2" ( // returns an Integer
)
These code segments show invalid uses of age because they do not
preserve the Integer type specification:
- definition comment in Person : "<ValueModel on: Integer>"
(same as above, this is OK)
- Person>>initialize : "ageHolder := ValueHolder on:
0.0" ( 0.0 is not an Integer )
- Person>>age: : "self ageHolder value: newAge"
(this is OK, but assumes that newAge is valid)
- "self age: 5.5" ( 5.5 is not an Integer )
- "self age tooYoung ifTrue: [...]" ( Integer s do
not understand tooYoung )
- "self age: self age / 2" ( / does not always return
an Integer )
Be careful to write code like the first set, not the second, in order
to keep a variable's type consistent.
3. Types of ValueModels
Typically, discussions about the Value Model framework center around
the class ValueModel , because it is the abstract class that defines
the basic behavior and interface of all ValueModel s . It is, however,
an abstract class; there are no instances of ValueModel to use.
A programmer must actually use instances of ValueModel 's subclasses.
In VisualWorks 1.0, the ValueModel hierarchy contains a number
of concrete subclasses. Their front end is always the same: a single
aspect named value that remembers the interests registered on that
value. What distinguishes them is their back ends: how they attach
to other models, how they hold/retrieve their values, and how they
convert and/or translate their values. By knowing what each one
does, a programmer can choose which one to use for the job at hand.
Here are some guidelines that describe how to use the different
types of ValueModel s and their associated classes:
3.1 Use a ValueHolder to hold a stand-alone object
Problem
How can I wrap ValueModel behavior around the objects in my instance
variables?
Context
ValueModel behavior is often desirable (see "1. What is a ValueModel?").
The ValueModel must store the object it is holding because there
are no other objects to do so.
The ValueModel does not need to perform any sort of conversion
or translation on the value; it should just return the value as
is.
Solution
Use a ValueHolder , the simplest and most commonly used type of
ValueModel . It will wrap the object within itself, thus giving
the object ValueModel behavior. Store the ValueHolder in the instance
variable.
Example
A domain model has multiple aspects; the value of each of these
is stored in an instance variable. Multiple application models may
need to share each of these values.
Make each instance variable a ValueHolder to store the aspect's
value. This will facilitate abstracting the value's aspect and make
it easy to register interest on the value, without introducing any
unnecessary overhead.
A simple way to wrap a ValueHolder around a value is to send the
value asValue (The message asValueModel would probably be more intuitive.).
So to set-up a domain model with a number of aspects in ValueHolders
(see "2. How to use ValueModels"), your initialize method
will look like this:
initialize
. . .
aspect1Holder := self aspect1DefaultValue asValue.
aspect2Holder := self aspect2DefaultValue asValue.
<etc.>
. . .
Then it will need getters for the aspect holders and getters and
setters for the aspect values.
3.2 Use an AspectAdaptor to hold an aspect of an object
Problem
How can I wrap ValueModel behavior around my retrieval of an aspect
of another model?
Context
ValueModel behavior is often desirable (see "1. What is a ValueModel?").
The ValueModel does not need to actually store the value because
it is already being stored by another model.
When that model's aspect's value changes (and the model notifies
its dependents), the ValueModel should update its value. If the
model does not notify its dependents when its aspect's value changes,
its dependents (including your ValueModel ) won't get updated.
The ValueModel does not need to perform any sort of conversion
or translation on the value; it should just return the value as
is.
Solution
Use an AspectAdaptor , popular because models with simple aspects
are popular. Unlike a ValueHolder (see "3.1 Use a ValueHolder
to hold a stand-alone object"), the object itself won't be
wrapped with ValueModel behavior. But the process of retrieving
and storing the object as an aspect of its container model will
be wrapped as a ValueModel . This is how this kind of ValueModel
is able to monitor the container model for changes in the aspect.
Example
A domain model has multiple aspects; the value of each of these
is stored in an instance variable. The developer who implemented
the domain model used simple aspects to store and retrieve the values,
but he didn't use ValueModel s to make registering dependencies
easy. Your application model needs to use some of these aspects.
Your application model will need an AspectAdaptor for each aspect
it wishes to share with the domain model. Let's say these are the
specifics for two of the aspects (these examples are from [Par93]):
| aspect name |
getter name |
setter name |
update aspect |
| name |
name |
name: |
#name |
| address |
getAddress |
setAddress: |
#getAddress |
(Note: The update aspect must always be the same
as the getter name. If the aspect you're adapting does not follow
this convention, use a PluggableAdaptor ; see "3.7 Use a PluggableAdaptor
to hold some part of an object".)
The initialize code to set-up these two adapters would be:
initialize
nameHolder := (AspectAdapter subject: domainModel sendsUpdates: true)
forAspect: #name.
addressHolder := (AspectAdapter subject: domainModel sendsUpdates: true)
accessWith: #getAddress assignWith: #setAddress.
Then it will need getters for the aspect holders and getters and
setters for the aspect values.
3.3 Use a BufferedValueHolder to delay the commit of a
new value
Problem
My view has a number of values. How can I allow the user to change
all of them without changing them in the application model, then
accept or cancel all of the changes at one time?
Context
This mechanism should be a ValueModel so that it will have the generic
aspect value . That way, it can be used to store any aspect necessary
(see "1.1 Use a ValueModel to generalize an object's aspect").
It should get its value from another ValueModel , so that it can
retrieve its value generically.
This mechanism will need a trigger with three positions: commit,
neutral, and flush. Nothing happens to the values in the application
model until the trigger flips out of neutral.
If multiple fields are to trigger simultaneously, their ValueModel
s will need to share a single trigger.
Solution
Use a BufferedValueHolder as a layer of separation between the model
and the ValueModel for a field. A regular ValueModel will immediately
store a new value in the model (and notify dependents accordingly).
However, a ValueModel stored inside a BufferedValueHolder will suspend
the new value until you say to commit it to the model.
Example
Let's say an application model has three fields and a pair of accept/cancel
buttons. You want to allow the user to edit the fields, but not
commit the edits to the model until the user presses accept. If
he presses cancel, the edits should be discarded and the fields
should display the original values from the model.
To implement this behavior, use two layers of ValueModel s instead
of just one. The top layer will consist of unbuffered ValueModel
s -- like ValueHolder , AspectAdapter , PluggableAdaptor , etc.
-- depending on how they must retrieve and translate their values.
The bottom layer, between the ValueModel s and the application model,
consists of BufferedValueHolder s , one for each ValueModel in the
top layer.
Connect the BufferedValueHolder s together with a single trigger
that is a ValueModel whose value is a Boolean or nil . As long as
the trigger is nil , nothing happens. If the trigger changes its
state to true , the BufferedValueHolder s commit their values; if
it changes to false , they discard their values and reset to the
original values from the ValueModel s . Thus the accept button should
set the trigger to true and cancel should set it to false .
One view could conceivably contain multiple groups of BufferedValueHolder
s , each group sharing its own trigger. This would allow each group
to be committed individually.
3.4 Use a RangeAdaptor to channel a number's range
Problem
How can I easily convert a real world quantity to a percentage such
that whenever the quantity changes, the percentage is automatically
recalculated?
Context
A quantity is often more easily understood by expressing it as a
percentage (a number between 0 and 100%).
A percentage quantity can easily be displayed in intuitive ways:
a dial, a needle on a gauge, etc. A group of percentage quantities
can be displayed together as a bar graph, a pie chart, etc.
When a quantity is confined to a range, it can easily be converted
into a percentage quantity.
The object holding a percentage quantity should be a ValueModel
so that it can be used to store any aspect necessary and the quantity
can be easily shared (see "1. What is a ValueModel?").
The getter and setter value and value: should return a percentage.
If the sender wants the original number within a range, it'll access
the value directly.
Solution
Use a RangeAdaptor , a ValueModel that converts a number in a specified
range into a percentage quantity. It sits between the ValueModel
holding the value within the range and the model wanting a percentage
quantity. The percentage quantity will be expressed as a number
between 0 and 1.
Example
Slider visual widgets use a RangeAdaptor to convert a number in
a specified range into a set range, 0 to 1. The RangeAdaptor must
know the minimum and maximum values in the range, and must know
the step size to use when converting the percentage back into a
number in the range. Because a RangeAdaptor is a ValueModel , when
the number in the range changes, the RangeAdaptor recalculates the
percentage and notifies its dependents automatically.
3.5 Use an IndexedAdaptor to hold a single index in a Collection
Problem
How can I wrap ValueModel behavior around a particular element in
a Collection ?
Context
ValueModel behavior is often desirable (see "1. What is a ValueModel?").
The ValueModel does not need to actually store the value because
it is already being stored by the Collection .
To hide the fact that this object is stored in a Collection , use
a ValueModel so that it will have the generic aspect value.
The ValueModel does not need to perform any sort of conversion
or translation on the value; it should just return the value as
is.
Solution
Use an IndexedAdaptor to give a Collection element the aspect value
. Since an IndexedAdaptor interfaces with the Collection via the
element's index, the Collection must understand at: and at:put :
(thus it must be an Array or an OrderedCollection ). The IndexedAdaptor
will also allow the element to be shared by multiple dependents.
If an IndexedAdaptor changes an element to another, the new element
will appear in the old element's position in the Collection .
Example
Let's say you have a list of Person elements and a view that displays
the fields for a Person. The fields should all be AspectAdapter
s on a selection channel. Since the list is a SequenceableCollection
, the user can walk through the list by incrementing an index pointer
into the list. Thus the selection channel should be a IndexedAdaptor
that is a ValueModel on an element in the list.
Here's how to initialize the IndexedAdaptor :
selectionChannel := (IndexedAdaptor subject: personList)
forIndex: 1.
The view will initially display the information for the first Person
in the list. To display Person n, set the IndexedAdaptor 's index
to n. To walk through the list, increment or decrement the index.
3.6 Use a SlotAdaptor to hold a single instance variable
Problem
How can I wrap ValueModel behavior around a particular instance
variable in an object without changing what kind of object an instance
variable holds?
Context
ValueModel behavior is often desirable (see "1. What is a ValueModel?").
If the nature of the instance variable can be changed from a variable
that holds a value to a variable that holds a ValueModel that holds
a value, use a ValueHolder (see "3.1 Use a ValueHolder to hold
a stand-alone object"). This will require changing some of
the object's methods, such as the getter and setter for the instance
variable, as well as any other methods that access the variable
directly, such as initialize .
If the instance variable is an aspect of the object, with getter
and setter methods to access the instance variable, use an AspectAdapter
(see "3.2 Use an AspectAdapter to hold an aspect of an object").
If there are other means in the object's public interface to get
and set the value of this instance variable, use a PluggableAdaptor
(see "3.7 Use a PluggableAdaptor to hold some part of an object").
The ValueModel does not need to actually store the value because
it is already being stored by another object.
To hide the fact that this object is an instance variable with
no accessor methods, use a ValueModel so that it will have the generic
aspect value.
The ValueModel does not need to perform any sort of conversion
or translation on the value; it should just return the value as
is.
Solution
Use a SlotAdaptor to give an object's instance variable the aspect
value. The SlotAdaptor will also allow the element to be shared
by multiple dependents. If the SlotAdaptor is used to change the
value, the object's instance variable will now have the new value.
(Note: Changing an instance variable out from
under an object in this manner is a bad idea. You should use a setter
method in the object's public interface to make such changes. Changing
an instance variable directly is a private procedure that could
make the object's state (or those of its dependents) inconsistent.
Whenever possible, use an AspectAdapter or a PluggableAdaptor ,
not a SlotAdaptor . )
Example
Let's say aThing contains the instance variable amount, but aThing
does not provide sufficient behavior for getting and setting amount.
You can attach a SlotAdaptor directly to the instance variable with
this code:
amountHolder := SlotAdaptor subject: aThing.
amountHolder forIndex:
(amountHolder subject class
allInstVarNames indexOf: 'amount').
amountHolder is now a ValueModel whose value is the exact same
object as the one in the instance variable amount. If the SlotAdaptor
puts a different object in that instance variable, aThing will never
know. The next time aThing uses that instance variable, the variable
will contain a different value than the last time aThing used it.
3.7 Use a PluggableAdaptor to hold some part of an object
Problem
How can I wrap ValueModel behavior around an arbitrary portion of
an object?
Context
ValueModel behavior is often desirable (see "1. What is a ValueModel?").
The ValueModel does not need to actually store the value because
it is already being stored by another object.
If the value is stored in the model as an aspect, use an AspectAdapter
(see "3.2 Use an AspectAdapter to hold an aspect of an object").
If the value is an element in a Collection , use an IndexedAdaptor
(see "3.5 Use an IndexedAdaptor to hold a single index in a
Collection").
If the value is stored in an instance variable that cannot be accessed
adequately through the model's behavior, use a SlotAdaptor (see
"3.6 Use a SlotAdaptor to hold a single instance variable").
Obtaining the value from the model may be tricky. The value may
need to be derived from explicit values in the model; to store it
back, the ValueModel may need to dissect the value to get its components
and store them into the model. The ValueModel may need to make a
series of decisions to decide if its value should be updated.
In fact, the ValueModel may not appear to hold a value at all,
but rather to be a detection system for an event that may occur.
Solution
Use a PluggableAdaptor to hold an arbitrary portion of an object
when no other kind of ValueModel will work. PluggableAdaptor is
a very brute force style of ValueModel that is overkill in many
cases. However, when the value that one model wants and that which
the other model has are very different, a PluggableAdaptor is often
able to bridge these differences whereas no other kind of ValueModel
can
Example 1
Let's say the domain model holds the aspects firstName and lastName
. The application model wants an instance of FullName that contains
the information in the domain model. To store a new name, the FullName
will have to be broken apart into the domain model's aspects. When
either of these aspects of the domain model changes, the FullName
in the application model will have to be updated accordingly.
(PluggableAdaptor on: self domianModel)
getBlock: [ :model |
FullName
firstName: model firstName
lastName: model lastName]
putBlock: [ :model :value "aFullName" |
model
firstName: value firstName;
lastName: value lastName]
updateBlock: [ :model :aspect :parameter |
aspect == #firstName |
aspect == #lastName].
Example 2
face="A.D. MONO"> ParcPlace has already implemented
some specialized messages; browse PluggableAdaptor to review them:
- collectionIndex : -- Makes the PluggableAdaptor work like an
IndexedAdaptor .
- getSelector:putSelector : -- Makes it work like an AspectAdapter
.
- performAction : -- Used to make a Button act like an ActionButton
.
- selectValue : -- Acts like a Boolean whose value reflects whether
or not the value being held is equal to aValue .
3.8 Use a TypeConverter to convert a value between types
Problem
Two objects wish to share a value, but one dependent wants it expressed
as one type and the other expects another type. How can the value
appear as the appropriate type of object for each dependent?
Context
When two dependents want two types of objects for the same value,
this assumes that the value can be converted from one type of object
to the other and back again. If not, they cannot share the same
value instance.
The value to be converted should be accessed via the generic aspect
value .
The converted value should be accessed via the generic aspect value
.
Solution
Use a TypeConverter to convert the type of object being held as
a value in one ValueModel into the type expected by the model. When
storing a new value, the TypeConverter will perform the inverse
conversion.
Note : The updateBlock for a TypeConverter is always true. TypeConverter
redefines value: not to send out updates. This way, the objects
at either end of the converter handle sending-out updates and the
converter just conveys them transparently.
Example 1
Let's say a couple of objects want to share a value that is a Number
, and a couple of other objects want to share the same value as
a String . Here's how:
valueAsNumberHolder := ValueHolder on: theNumber.
converter := (TypeConverter on: valueAsNumberHolder)numberToText.
valueAsStringHolder := (AspectAdapter subject: converter sendsUpdates: true)
forAspect: #value.
With this setup, objects that want to share the value as a Number
register their interests on valueAsNumberHolder ; those which want
the value as a String register with valueAsStringHolder .
Example 2
Many of the most common conversions have already been defined for
you, like numberToText . Browse TypeConverter to see others like
dateToText , stringOrSymbolToText , and stringToNumber .
3.9 Use a ScrollValueHolder to hold a position in n-dimensional
space
Problem
What object should I use to keep track of how much a point's position
on a grid has changed?
Context
A single Number will be insufficient. It can keep track of the point's
distance from the grid's origin, but that will not be enough information
to calculate the point's unique position on the grid. There will
need to be one Number for each dimension of the grid.
Just knowing the position on the grid (the point) is insufficient.
If the point must move across the grid in quantum steps, the size
of the minimum step must be available. When the point tries to move,
its movements must be constrained to those which are a multiple
of the grid step size.
Solution
Use a ScrollValueHolder to hold the scroll position of a point (which
may or may not be an instance of Point , which is two-dimensional).
Set the grid to be the minimum size of each step the point must
take when it moves.
Example
ScrollWrapper s are the heart of how the scroll bars on a view work;
they connect the scroll bars to the view. The ScrollWrapper must
remember what its position is; the position is stored in a ScrollValueHolder
. The view's scroll position without a valid scroll position being
stored in the ScrollValueHolder .
Before storing a new scroll position, the ScrollValueHolder will
round it off to the nearest grid position. This position is incremented/decremented
by one grid size when a scroll bar button is pushed. The position
will be adjusted, if necessary, to make it stay within the wrapper's
bounds.
The value of the ScrollValueHolder can be adjusted programatically.
To auto-scroll the view for the user, store a new scroll position
into the ScrollValueHolder . The new value will be validated, stored,
and the wrapper will be informed of the new value, at which time
the wrapper will scroll the view.
3.10 Use a SelectionInList to hold a list and its selection
Problem
How can I set-up a list such that it can tell me which item is currently
being used?
Context
A Collection knows which items it contains, and can allow you to
reference one item at a time, but cannot tell you which one is currently
being referenced.
An item cannot tell you which Collection it is a part of. In fact,
it may not be part of any Collection , or may be part of several.
Thus another object will need to contain the list and keep track
of which item is currently being used. It should work with any list
and selection, regardless of what their aspects are.
You may need to know when the currently referenced item changes.
You may need to know when the items in the list change.
Solution
Use a SelectionInList to track a list and its current selection.
If you register your interest in one of its aspects ( list or selectionIndex
), it will notify you of changes.
(Editorial comment: SelectionInList 's interface
actually isn't as clean as it ought to be. It implements two aspects,
list and selectionIndex , using two ValueModel s , listHolder and
selectionIndexHolder . It then implements a third aspect, selection
, but does not implement it using a ValueModel . So there is no
selectionHolder to register interest with, nor to send out selection
changed aspects. See also "2.2 Use ValueModel chains instead
of onChangeSend:to:".)
Example
Here's how to create a SelectionInList of Person objects:
personSelectionInList := (SelectionInList with: self peopleList)
selectionIndex: 1.
This will contain the list of Person s; the first one is selected.
To be notified when the list is replaced with another, register
your interest with listHolder . Register interest with selectionIndexHolder
and you'll be notified when the selection (actually the selection
index) changes. To change its list, selection index, or selection
programatically, use list: , selectionIndex : , and selection: .
Appendices
References
[Par93] ParcPlace Systems, Sunnyvale, CA. The discrete charm of
the ValueModel. Parc Notices, pp. 1, 89, Summer 1993, Vol.
4, No. 2.
|