Previous Contents
A A more natural and flexible solution to the subject/observer pattern


The general idea behind the subject/observer pattern is to realize a complex operation by the tight collaboration of several objects of different classes. Here, the operations are implemented in the subject while all the control remains in the observer. In this regards, the use of type event to communicate between the subject and the observer is surprising and not so modular: typically, any refinement of the communication pattern will imply a simultaneous refinement of the event class used to communicate between the subject and the observer. Actually, the event class is used to pack in an extensible sum type messages that are then pattern-matched and treated by the observer class. This weakens the security, since the system does not check for exhaustive pattern-matching. A first solution in Ocaml is to represent events as a variant instead of as an object. This is described in appendix 1. Yet, a new, direct, and safer implementation of the subject/observer pattern would let the subject notify the observer by calling the appropriate message of the observer; this is described in section A.2.

A.1 Representing events in a variant type

In is part we show a slight improvement of the example given in section 1 that represent actions as variants. The root classes subject and observer are left unchanged. The class window is then using a variant `Move to signal a move.
class ['O, 'E] window = 
  object (self) 
    inherit ['O, 'E] subject
    val mutable position = 0
    method move d = position <- position + d; self#notify `Move
    method draw = Printf.printf "{Position = %d}" position;
  end;;
class ['a, 'b] window :
  object ('c)
    constraint 'a = < at_notification : 'c -> 'b -> unit; .. >
    constraint 'b = [> `Move]
    val mutable observers : 'a list
    val mutable position : int
    method add_observer : 'a -> unit
    method draw : unit
    method move : int -> unit
    method notify : 'b -> unit
  end
Note that, since variants are extendible, the role of the function event coercing actions to the type of events is played by the primitive operations on variants.

Correspondingly, the class manager is changed to:
class ['S, 'E] manager =
  object
    inherit ['S, 'E] observer 
    method at_notification s e = match e with `Move -> s#draw | _ -> ()
  end;;
class ['a, 'b] manager :
  object
    constraint 'a = < draw : unit; .. >
    constraint 'b = [> `Move]
    method at_notification : 'a -> 'b -> unit
  end
Note the _ that allows at_notification to be called with different tags, in which case the default behavior is to ignore the message. This is reminded in the type constraint 'b = [> `Move], which says that the variant type 'b can have tag `Move with a value of type unit, or any other tag with a value of any type.

A refined version of windows simply use other tags as needed:
class ['O, 'E] big_window =
  object (self)
    inherit ['O, 'E] window
    val mutable size = 1
    method resize x = size <- size + x; self#notify `Move 
    val mutable top = false
    method raise = top <- true; self#notify (`Resize false)
    method draw = Printf.printf "{Position = %d; Size = %d}" position size;
  end;;
Similarly, the refined version of the manager may respond to the new tags:
class ['S, 'E] big_manager =
  object 
    inherit ['S, 'E] manager as super
    method at_notification s e =
      match e with `Resize b -> s#raise | _ -> super#at_notification s e
  end;;
Here a test showing that classes can correctly be combined: (We assume an obvious redefinition of class trace_observer).
let w = new big_window in w#add_observer (new big_manager); w#resize 2; w#move 1;;
{Position = 0; Size = 3}{Position = 1; Size = 3}- : unit = ()
This new version of the subject/observer pattern is simpler that the previous one, by avoiding the useless encoding of variants into objects. However, it does not provide much more security. In particular, the at_notification method will accept all tags in prevision of further extension. That is, a forgotten specialization of the method at_notification will not produce an typechecking error, but will simply ignore the notification. Below is a solution that fixes this problem and that is thus more secure. It is also simpler.

A.2 Using different methods for each kind of event

.

The method notify now takes a message to be send to all registered observers.
class ['O] subject =
  object (self : 'mytype)
    val mutable observers : 'O list = []
    method add_observer obs = observers <- obs :: observers
    method notify (message : 'O -> 'mytype -> unit) = 
      List.iter (fun obs -> message obs self) observers
  end;;
class ['a] subject :
  object ('b)
    val mutable observers : 'a list
    method add_observer : 'a -> unit
    method notify : ('a -> 'b -> unit) -> unit
  end
The class observer is initially empty.
class ['S] observer =  object  end;;
A window/manager is obtained by inheriting form the above pattern. For instance, the class window may have a method move whose code will in turn notify all observers with a messaged moved. Correspondingly, the window-manager is extended to accept messages moved: it then simply calls back the method draw of the window that signaled the move (called the moved message of the manager):
class ['O] window =
  object (self : 'mytype) 
    inherit ['O] subject
    val mutable position = 0
    method move d = position <- position + d; self#notify (fun x -> x#moved)
    method draw = Printf.printf "[Position = %d]" position;
  end;;

class ['S] manager =
  object
    inherit ['S] observer 
    method moved (s : 'S) : unit =  s#draw
  end;;
An instance of this pattern is well-typed because the manager correctly treats all messages sent by the window.
let w = new window in w#add_observer (new manager); w#move 1;;
[Position = 1]- : unit = ()
This would not be the case if, for instance, we forgot to implement the moved message in class manager.

The pattern can further be extended. Instead of extending the event class, as in section 1 one can more appropriately notify the observers on another method ("resized" in the example below):
class ['O] big_window =
  object (self)
    inherit ['O] window as super
    val mutable size = 1
    method resize x = size <- size + x; self#notify (fun x -> x#resized true)
    method draw = super#draw; Printf.printf "[Size = %d]" size; 
  end;;

class ['S] big_manager =
  object 
    inherit ['S] manager as super
    method resized b (s:'S) = if b then s#draw 
  end;;
To check the flexibility, we also add a trace_observer that is a refinement of the class observer:
class ['S] trace_observer = 
  object 
    inherit ['S] observer
    method resized (b:bool) (s:'S) = print_string "<R>"
    method moved (s:'S) = print_string "<M>"
  end;;
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 = ()



Previous Contents