Observers Of Multiple Subjects
In this variation of the Observer pattern, an observer can observe
multiple subjects.
In the original example, a digital clock observed only a clock timer.
When a tick happened, the clock timer subject would notify the digital
clock observer, who would display the time.
We now introduce another subject, a battery, that notifies its observer
when it is drained or charged. The digital clock simultaneously
observes both a timer and a battery, displaying a message as it is
notified by each.
Implementation
The clock timer subject is unchanged from the original example.
The battery subject features operations to charge and drain the battery,
and a selector to query whether the battery is low on power:
package Batteries is
type Battery_Type is new Root_Subject_Type with private;
procedure Charge (Battery : in out Battery_Type);
procedure Drain (Battery : in out Battery_Type);
function Is_Low (Battery : in Battery_Type) return Boolean;
...
end Batteries;
The Charge and Drain operations are analogous to Tick for the clock
timer. When you charge (or drain) the battery, it sets its internal
state, then notifies its observers:
procedure Charge (Battery : in out Battery_Type) is
begin
Battery.State := 1;
Notify (Battery); <--
end;
The digital clock observer has been modified so that it can observe both
a clock timer and a battery. Per our idiom for observers, it is
declared as a limited private type with an access discriminant for each
subject:
type Digital_Clock
(Timer : access Clock_Timer'Class;
Battery : access Battery_Type'Class) is limited private;
The full view of the type has also been changed. Recall that in the
earlier implementation, Digital_Clock type privately derived from
Root_Observer_Type, extending it with a component to automatically bind
the clock observer to the timer subject.
Instead of privately deriving from Root_Observer_Type, the digital clock
type is now implemented as a (limited) record with two observer
components:
type Digital_Clock
(Timer : access Clock_Timer'Class;
Battery : access Battery_Type'Class) is
limited record
Timer_Obs : aliased Timer_Obs_Type (Digital_Clock'Access);
Battery_Obs : aliased Battery_Obs_Type (Digital_Clock'Access);
...
Control : Control_Type (Digital_Clock'Access);
end record;
Note that the order of declaration of record components is crucial. The
Control component is going to attach the observer components to the
respective subject during the elaboration of a digital clock object.
This requires that the observer components be elaborated prior to
elaboration of the Control component.
The language specifies that components are elaborated in the order of
their declaration in the component list. Therefore, to ensure that the
observer components are elaborated prior to the Control component, we
declare them earlier in the list.
We also declare the full view of the Digital_Clock type as limited,
which allows us to use T'Access to designate the "current instance" of
the type. This is how we bind the observer components (and the Control
component) to their containing instance.
The observer types extend the root observer type with an access
discriminant designating the clock:
type Timer_Obs_Type
(Clock : access Digital_Clock) is
new Root_Observer_Type with null record;
procedure Update (Observer : access Timer_Obs_Type);
type Battery_Obs_Type
(Clock : access Digital_Clock) is
new Root_Observer_Type with null record;
procedure Update (Observer : access Battery_Obs_Type);
The Update for the Timer observer is the same as before. When signalled
by the timer, the observer queries the timer for the current time:
procedure Update (Observer : access Timer_Obs_Type) is
Clock : Digital_Clock renames Observer.Clock.all;
Timer : Clock_Timer'Class renames Clock.Timer.all;
<get timer time and display it>
end Update;
The Update for the battery observer queries the battery to determine
whether the charge is low, and prints out a message:
procedure Update (Observer : access Battery_Obs_Type) is
Clock : Digital_Clock renames Observer.Clock.all;
Battery : Battery_Type'Class renames Clock.Battery.all;
...
begin
if Is_Low (Battery) then
<print message>
...
end Update;
The Control_Type derives from Limited_Controlled and overrides
Initialize to attach both observer components to their subjects:
procedure Initialize (Control : in out Control_Type) is
Clock : Digital_Clock renames Control.Clock.all;
begin
Attach (Observer => Clock.Timer_Obs'Access,
To => Clock.Timer);
Attach (Observer => Clock.Battery_Obs'Access,
To => Clock.Battery);
end Initialize;
Finalize is implemented similarly.
Note that the observer is an access parameter in Attach (and Detach), so
this requires we take the 'Access of the observer component of the
clock. In order to do so, the component must be declared aliased.
The following code is ready for GNAT Chop:
--STX
package body Batteries is
procedure Charge (Battery : in out Battery_Type) is
begin
Battery.State := 1;
Notify (Battery);
end;
procedure Drain (Battery : in out Battery_Type) is
begin
Battery.State := Battery.State + 1;
Notify (Battery);
end;
function Is_Low (Battery : in Battery_Type) return Boolean is
begin
return Battery.State > 3;
end;
end Batteries;
with Subjects_And_Observers; use Subjects_And_Observers;
package Batteries is
type Battery_Type is new Root_Subject_Type with private;
procedure Charge (Battery : in out Battery_Type);
procedure Drain (Battery : in out Battery_Type);
function Is_Low (Battery : in Battery_Type) return Boolean;
private
type Battery_Type is
new Root_Subject_Type with record
State : Positive := 1;
end record;
end Batteries;
with Ada.Calendar; use Ada.Calendar;
package body Clock_Timers is
procedure Tick (Timer : in out Clock_Timer) is
Current_Time : constant Time := Clock;
Current_Seconds : constant Natural :=
Natural (Seconds (Current_Time));
begin
Timer.Hour := Current_Seconds / 3600;
Timer.Minute := (Current_Seconds - 3600 * Timer.Hour) / 60;
Timer.Second :=
Current_Seconds -
3600 * Timer.Hour -
60 * Timer.Minute;
Notify (Timer);
end Tick;
function Get_Hour (Timer : Clock_Timer) return Hour_Number is
begin
return Timer.Hour;
end;
function Get_Minute (Timer : Clock_Timer) return Minute_Number is
begin
return Timer.Minute;
end;
function Get_Second (Timer : Clock_Timer) return Second_Number is
begin
return Timer.Second;
end;
end Clock_Timers;
with Subjects_And_Observers; use Subjects_And_Observers;
package Clock_Timers is
type Clock_Timer is new Root_Subject_Type with private;
procedure Tick (Timer : in out Clock_Timer);
subtype Hour_Number is Natural range 0 .. 23;
function Get_Hour (Timer : Clock_Timer) return Hour_Number;
subtype Minute_Number is Natural range 0 .. 59;
function Get_Minute (Timer : Clock_Timer) return Minute_Number;
subtype Second_Number is Natural range 0 .. 59;
function Get_Second (Timer : Clock_Timer) return Second_Number;
private
type Clock_Timer is
new Root_Subject_Type with record
Hour : Hour_Number;
Minute : Minute_Number;
Second : Second_Number;
end record;
end Clock_Timers;
with Ada.Text_IO; use Ada.Text_IO;
package body Digital_Clocks is
procedure Set_Name
(Clock : in out Digital_Clock;
Name : in String) is
begin
Clock.Name (1 .. Name'Length) := Name;
Clock.Name_Length := Name'Length;
end;
procedure Update (Observer : access Timer_Obs_Type) is
Clock : Digital_Clock renames Observer.Clock.all;
Timer : Clock_Timer'Class renames Clock.Timer.all;
Hour_Image : constant String :=
Integer'Image (Get_Hour (Timer) + 100);
Minute_Image : constant String :=
Integer'Image (Get_Minute (Timer) + 100);
Second_Image : constant String :=
Integer'Image (Get_Second (Timer) + 100);
Clock_Image : constant String :=
Hour_Image (3 .. Hour_Image'Last) & ":" &
Minute_Image (3 .. Minute_Image'Last) & ":" &
Second_Image (3 .. Second_Image'Last);
Name : String renames
Clock.Name (1 .. Clock.Name_Length);
begin
Put_Line ("Clock named '" & Name & "' shows time " & Clock_Image);
end Update;
procedure Update (Observer : access Battery_Obs_Type) is
Clock : Digital_Clock renames Observer.Clock.all;
Battery : Battery_Type'Class renames Clock.Battery.all;
Name : String renames
Clock.Name (1 .. Clock.Name_Length);
begin
if Is_Low (Battery) then
Put_Line
("Battery charge is getting low for clock named '" &
Name &
"'");
else
Put_Line
("Battery is charged for clock named '" &
Name &
"'");
end if;
end Update;
procedure Initialize (Control : in out Control_Type) is
Clock : Digital_Clock renames Control.Clock.all;
begin
Attach (Observer => Clock.Timer_Obs'Access,
To => Clock.Timer);
Attach (Observer => Clock.Battery_Obs'Access,
To => Clock.Battery);
end Initialize;
procedure Finalize (Control : in out Control_Type) is
Clock : Digital_Clock renames Control.Clock.all;
begin
Detach (Observer => Clock.Timer_Obs'Access,
From => Clock.Timer);
Detach (Observer => Clock.Battery_Obs'Access,
From => Clock.Battery);
end Finalize;
end Digital_Clocks;
with Clock_Timers; use Clock_Timers;
with Batteries; use Batteries;
with Subjects_And_Observers;
with Ada.Finalization;
package Digital_Clocks is
pragma Elaborate_Body;
type Digital_Clock
(Timer : access Clock_Timer'Class;
Battery : access Battery_Type'Class) is limited private;
procedure Set_Name
(Clock : in out Digital_Clock;
Name : in String);
private
use Ada.Finalization;
type Control_Type (Clock : access Digital_Clock) is
new Limited_Controlled with null record;
procedure Initialize (Control : in out Control_Type);
procedure Finalize (Control : in out Control_Type);
use Subjects_And_Observers;
type Timer_Obs_Type
(Clock : access Digital_Clock) is
new Root_Observer_Type with null record;
procedure Update (Observer : access Timer_Obs_Type);
type Battery_Obs_Type
(Clock : access Digital_Clock) is
new Root_Observer_Type with null record;
procedure Update (Observer : access Battery_Obs_Type);
type Digital_Clock
(Timer : access Clock_Timer'Class;
Battery : access Battery_Type'Class) is
limited record
Timer_Obs : aliased Timer_Obs_Type (Digital_Clock'Access);
Battery_Obs : aliased Battery_Obs_Type (Digital_Clock'Access);
Name : String (1 .. 50);
Name_Length : Natural := 0;
Control : Control_Type (Digital_Clock'Access);
end record;
end Digital_Clocks;
package body Subjects_And_Observers is
procedure Notify (Subject : in out Root_Subject_Type'Class) is
Observer : Observer_Access := Subject.Head;
begin
while Observer /= null loop
Update (Observer);
Observer := Observer.Next;
end loop;
end Notify;
procedure Attach
(Observer : access Root_Observer_Type'Class;
To : access Root_Subject_Type'Class) is
begin
Observer.Next := To.Head;
To.Head := Observer.all'Unchecked_Access;
end;
procedure Detach
(Observer : access Root_Observer_Type'Class;
From : access Root_Subject_Type'Class) is
OA : constant Observer_Access :=
Observer.all'Unchecked_Access;
Prev : Observer_Access;
Index : Observer_Access;
begin
if From.Head = OA then
From.Head := From.Head.Next;
else
Prev := From.Head;
Index := From.Head.Next;
while Index /= OA loop
Prev := Index;
Index := Index.Next;
end loop;
Prev.Next := Index.Next;
end if;
end Detach;
end Subjects_And_Observers;
package Subjects_And_Observers is
pragma Preelaborate;
type Root_Subject_Type is
abstract tagged limited private;
procedure Notify (Subject : in out Root_Subject_Type'Class);
type Root_Observer_Type is
abstract new Root_Subject_Type with private;
procedure Update (Observer : access Root_Observer_Type) is abstract;
procedure Attach
(Observer : access Root_Observer_Type'Class;
To : access Root_Subject_Type'Class);
procedure Detach
(Observer : access Root_Observer_Type'Class;
From : access Root_Subject_Type'Class);
private
type Observer_Access is access all Root_Observer_Type'Class;
pragma Suppress (Access_Check, On => Observer_Access);
type Root_Subject_Type is
abstract tagged limited record
Head : Observer_Access;
end record;
type Root_Observer_Type is
abstract new Root_Subject_Type with record
Next : Observer_Access;
end record;
end Subjects_And_Observers;
with Clock_Timers; use Clock_Timers;
with Batteries; use Batteries;
with Digital_Clocks; use Digital_Clocks;
with Ada.Text_IO; use Ada.Text_IO;
procedure Test_Observers is
Timer : aliased Clock_Timer;
Battery : aliased Battery_Type;
procedure Do_Ticks is
begin
delay 1.0;
Tick (Timer);
Drain (Battery);
delay 2.0;
Tick (Timer);
Drain (Battery);
delay 3.0;
Tick (Timer);
Drain (Battery);
New_Line;
Charge (Battery);
New_Line;
end Do_Ticks;
Clock : Digital_Clock (Timer'Access, Battery'Access);
begin
Set_Name (Clock, Name => "London");
Do_Ticks;
declare
Another_Clock : Digital_Clock (Timer'Access, Battery'Access);
begin
Set_Name (Another_Clock, Name => "Los Angeles");
Do_Ticks;
end;
Do_Ticks;
end Test_Observers;
Contributed by: Matthew Heaney
Contributed on: May 24, 1999
License: Public Domain
Back