1 |
The detailed example of subject/observers |
|
Virtual types are well illustrated by the example of the subject/observer
programming pattern. We simultaneously describe this example, its code, and
its typing in Ocaml. Since Ocaml has type inference, the user does not need
to write types except type parameters of classes and
appropriate type constraints to bind them in class bodies.
All the examples
of this section are written in the language Ocaml [Ler96]
and have been processed automatically by the toplevel interaction loop. We
only show the output in italic when needed.
The subject/observer example can be used when one or several observers need
to watch the state of one or several subjects or, more generally, when
objects of one class need to pass themselves to objects of another class
where both classes need to remain extensible.
We first define the skeleton of the pattern that implements the interaction
between the subject and the observer ignoring all other operations.
The subject reports actions to its
observers. Thus, it must remember the list of its observers in a field
variable. It should also provide some methods to add or remove observers.
More importantly, the subject has a distinguished method notify
used to
forward information to all of its observers (meanwhile adding itself as an
extra argument so that the observers can call the subject back).
'O'E
class ['O, 'E] subject =
object (self)
val mutable observers : 'O list = []
method add_observer obs = observers <- obs :: observers
method notify (e : 'E) =
List.iter (fun obs -> obs#at_notification self e) observers
end;;
class ['a, 'b] subject :
object ('c)
constraint 'a = < at_notification : 'c -> 'b -> unit; .. >
val mutable observers : 'a list
method add_observer : 'a -> unit
method notify : 'b -> unit
end
The inferred type of class subject
is parameterized over two variables
'a
and 'b
standing for the types of observers and of watched events,
and self type 'c
.
The type of observers 'a
is constrained to be an object type with
at least a method at_notification
of type 'c -> 'b -> unit
. The
dots ``..
'' in the constraint are a so-called row variable [Wan87, Rém94] that is kept anonymous in Ocaml for
simplicity of presentation. Thus, 'a
may be the type of an object with
more methods, for instance in a subclass. The class also has a field
observers
of type 'a list
and two
methods add_observer
and notify
of respective types
'a -> unit
and 'b -> unit
. It is also implicit from the class
type that the type of self bound to 'c
is the type of an object with
at least two methods add_observers
and notify
with their
respective types.
The class observer
is simpler. Mainly, it possesses a method
at_notification
to receive information from the subjects and
determine what to do next. The default behavior of the method
at_notification
is to do nothing, and the method will likely be
refined in subclasses.
class ['S, 'E] observer = object method at_notification (s : 'S) (e : 'E) = () end;;
The difficulty of this pattern usually lies in preserving the ability to
inherit from this general pattern to create new, real instances. This is
easy in Ocaml, as we illustrate on the example of a window manager. We
first define the type of events used to communicate between a window and a
manager. We make the event an object with an action
property, so that
the type of events can still be refined in subclasses2 (In Ocaml, it would be more
appropriate to use open variants to represent events, which we show in
appendix ; we keep the full
object-oriented solution here to ease comparison with other languages.)
type action = Move | Lower | Raise;;
class event x = object method action : action = x end;;
The window executes all
sorts of graphical operations, including its own display; whenever
necessary, the window will notify the manager that some particular task has
been completed. To allow further refinements of events, we abstract
the class window over a function that coerces actions to events.
class ['O, 'E] window (event : action -> 'E) =
object (self)
inherit ['O, 'E] subject
val mutable position = 0
method move d = position <- position + d; self#notify (event Move)
method draw = Printf.printf "[Position = %d]" position;
end;;
The manager watches events and defines the next task
to be accomplished depending on both the event and its sender. This may
include replying to the sender. For instance, when the observer is informed
that the window has moved, it may tell the window to redisplay itself.
class ['S, 'E] manager =
object
inherit ['S, 'E] observer
method at_notification s e = match e#action with Move -> s#draw | _ -> ()
end;;
Here is an example:
let window = new window (new event) in
window#add_observer (new manager); window#move 1;;
[Position = 1]- : unit = ()
Classes window
and manager
can be further extended by inheritance. We
first refine the type of events as suggested above. Then, we refine the
window class with a new behavior.
class big_event b x = object inherit event x method resized : bool = b end;;
class ['O] big_window() =
object (self)
inherit ['O, big_event] window (new big_event false) as super
val mutable size = 1
method resize x = size <- size + x; self#notify (new big_event true Raise)
method draw = super#draw; Printf.printf "[Size = %d]" size;
end;;
Here, we have definitely fixed the type of events for simplicity; of course,
we could have kept it parametric as we did for simple windows. The behavior
of the manager can also be refined, independently.
class ['S] big_manager =
object
inherit ['S, big_event] manager as super
method at_notification s e =
if e#resized then s#draw else super#at_notification s e
end;;
Note that the big windows are not bound to be managed by big managers.
In particular, one may freely choose any of the following combinations
(the missing one would fail to type, since a big manager requires its
window to have a method draw
):
(new window (new big_event false))#add_observer (new manager);;
(new big_window())#add_observer (new big_manager);;
(new big_window())#add_observer (new manager);;
Classes manager
and window
are defined independently. This is
important, since otherwise every combination would require a new definition.
More interestingly, we can also define another, entirely unrelated,
observer. This strengthens the idea that the class window
should not
be paired with the class manager
.
For instance, it is easy to implement an observer that simply traces all
events.
class ['S] trace_observer =
object
inherit ['S, big_event] observer
method at_notification s e =
Printf.printf "<%s>"
(match e#action with Lower -> "L" | Raise -> "R" | Move -> "M")
end;;
Here is an example combining all features:
let w = new big_window() in
w#add_observer (new big_manager); w#add_observer (new trace_observer);
w#resize 2; w#move 1;;
<R>[Position = 0][Size = 3]<M>[Position = 1][Size = 3]- : unit = ()
The example could be continued with many variations and refinements.
We also show another, safer implementation of the subject/observer in
appendix A.