Previous Contents Next
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
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
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, Rm94] 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;
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 =
    inherit ['S, 'E] observer 
    method at_notification s e = match e#action with Move -> s#draw | _ -> ()
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; 
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 =
    inherit ['S, big_event] manager as super
    method at_notification s e =
      if e#resized then s#draw else super#at_notification s e
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 = 
    inherit ['S, big_event] observer
    method at_notification s e =
      Printf.printf "<%s>"
        (match e#action with Lower -> "L" | Raise -> "R" | Move -> "M")
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.

Previous Contents Next