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 = ()