Sunday, 11 October, 2009

The Observer Design Pattern in Delphi – Push

A summary and [my rendition of] the Delphi code taken from the second chapter of the book, Head First Design Patterns from O’Reilly.
As I mentioned in the first post in this series, I’m studying Design Patterns using the Head First Design Patterns book from O’Reilly.   As part of this learning process I’m working through the existing examples written in Java and recreating them in Delphi.  This is the third post in the series and the second that actually deals with one of the design patterns.  The first post in the series also provides a list of additional resources on Design Patterns in Delphi that I’ve managed to track down.  I must mention again that I don’t plan on teaching you design patterns, as that would be quite presumptuous of me considering that I’m just a student of them myself.  I do intend to provide simply an overview of what I’ve learned and the resulting code I produced in the process.


The Observer Pattern ... (a subset of the asynchronous publish/subscribe pattern) is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.  It is mainly used to implement distributed event handling systems.
-- Wikipedia

The Observer Pattern ... defines a one to many dependency between objects so that when one object changes state, all of it’s dependants are notified and updated automatically.
-- Head First Design Patterns
There was a bit of discussion with the Strategy Pattern post that I did last time on my use of Interfaces.  This time, because a pattern is a pattern is a pattern and should be applicable whatever code structure you decide to use, I decided to stick to classes alone.  In this instance, and I expect in more of them to come should I make this a rule, it pushed me into areas of writing code that I had yet to experience, which was [some] fun.  My only concern with it is that this is not a direct translation of the Java code, which is still my sole intent.
This one was fun to work with.  It’s brain-dead simple and really easy to understand. Putting it into practise, depending on your personal coding standards/methods can present some challenges.  Me, I’m an everything in it’s own unit kinda guy and the infamous “circular reference” warning and I got to be good pals ... yet again.  There was more than once I wanted to shove it all in one unit and get it over with, but I eventually hammered out a solution that works.
There are a few things that I want to clarify before I start shoving code at you:
  • the title of this post is “Push” and that’s the style of Observer that is coded this time. I plan on doing a “Pull” article before I move on to the next pattern. I think the significant difference between push/pull is not emphasised in the book as much as it should be,
  • in the book, they start using the “built in Java Observer” units on page 64, I stopped there,
  • I, initially, didn’t understand why they called the Observable super-class “Subject”, but having read more on the pattern have come to understand the intent.  I had already created my classes at that point and taken license by using the term “Observable” and will stay with that decision.  I hope it’s not confusing.
  • the book is explicit in explaining how Observers register/subscribe or unregister/unsubscribe to the Observable/Subject when they want but, the code provided registers the Observers when they are created and the code required for an Observer to subscribe/unsubscribe at will is missing.  The Pull example I do next will have this feature.  Having said all this ... I want to ensure that I also make it clear that patterns are flexible and there aren’t only two answers to the question.  It’s the principle that’s important.
My rendition of the Java classes [with appropriate notations] are as follows:

The Observable [Subject] Class

unit uObservable;

interface

uses uObserver;

type
  TObservable = class(TObject)
    procedure NotifyObservers; virtual; abstract;
    procedure RegisterObserver(Observer: TBaseObserver); virtual; abstract;
    procedure RemoveObserver(Observer: TBaseObserver); virtual; abstract;
  end;

implementation

end.

The Observer Class

Next I built the abstract Observer class, TDisplayObserver, which is derived from TBaseObserver. 
unit uObserver;

interface

type
  TBaseObserver = class(TObject)
  public
    procedure UpDate(Temp: Single; Humid: Single; Press: Single); virtual; abstract;
  end;

  TDisplayObserver = class(TBaseObserver)
  public
    procedure Display; virtual; abstract;
  end;

implementation

end.
Note that the TDisplayObserver class is not only an abstract class but a compound abstract class. Any descendant class has to implement both the Display and Update methods.  We’ve done this to get around the lack of multiple inheritance in Delphi but required to closely follow the Java code supplied.  The other way around compound classes is a class that implements multiple Interfaces.  In addition, the Observable/Subject object don’t need to know any more about Observers than the have a UpDate method whereas the Observers all have to have both an UpDate method and a Display method.

The WeatherStation Class

unit WeatherStation;

interface

uses
  SysUtils, Classes, uObservable, uObserver;

type
  TWeatherStation = class(TObservable)
  private
    Observers: TList;
    FTemperature: Single;
    FHumidity: Single;
    FPressure: Single;
    function GetTemperature: Single;
    function GetHumidity: Single;
    function GetPressure: Single;
    procedure MeasurementsChanged;
  public
    property Humidity: Single 
      read GetHumidity 
      write FHumidity;
    property Pressure: Single
      read GetPressure
      write FPressure;
    property Temperature: Single
      read GetTemperature
      write FTemperature;
    constructor Create;
    destructor Destroy; override;
    procedure NotifyObservers; override;
    procedure RegisterObserver(Observer: TBaseObserver); override;
    procedure RemoveObserver(Observer: TBaseObserver); override;
    procedure SetMeasurements;
  end;

implementation

constructor TWeatherStation.Create;
begin
  inherited;
  Observers := TList.Create;
  Writeln('The Weather Station is up and running.');
end;

destructor TWeatherStation.Destroy;
begin
  Observers.Free;
  inherited;
end;

function TWeatherStation.GetHumidity: Single;
begin
  Result := FHumidity;
end;

function TWeatherStation.GetPressure: Single;
begin
  Result := FPressure;
end;

function TWeatherStation.GetTemperature: Single;
begin
  Result := FTemperature;
end;

procedure TWeatherStation.MeasurementsChanged;
begin
  NotifyObservers;
end;

procedure TWeatherStation.NotifyObservers;
var
  i: Integer;
begin
  if Assigned(Observers) then
    begin
      for i := 0 to Observers.Count - 1 do
        TBaseObserver(Observers[i]).Update(FTemperature, FHumidity, FPressure);
    end;
end;

procedure TWeatherStation.RegisterObserver(Observer: TBaseObserver);
begin
  if Observer is TBaseObserver then
    Observers.Add(Observer);
end;

procedure TWeatherStation.RemoveObserver(Observer: TBaseObserver);
begin
  if (Observer is TBaseObserver) and (Observers.IndexOf(Observer) <> -1) then
    Observers.Remove(Observer);
end;

procedure TWeatherStation.SetMeasurements;
begin
  Writeln('Enter the values as prompted and press Enter after each one.');
  Write('Current Temperature   [F]: ');
  Readln(FTemperature);
  Write('Current Humidity      [%]: ');
  Readln(FHumidity);
  Write('Current Pressure:  [" Hg]: ');
  Readln(FPressure);
  MeasurementsChanged;
end;

end.

Current Conditions Display

unit CurCondDisplay;

interface

uses SysUtils, uObserver, WeatherStation;

type
  TCurrentConditions = class(TDisplayObserver)
  private
    Temperature: single;
    Humidity: single;
    Pressure: single;
  public
    constructor Create(Source:TWeatherStation);
    procedure Update(Temp: Single; Humid: Single; Press: Single); override;
    procedure Display; override;
  end;

implementation

constructor TCurrentConditions.Create(Source:TWeatherStation);
begin
  inherited Create;
  Source.RegisterObserver(Self);
end;

procedure TCurrentConditions.Display;
begin
  WriteLn;
  Writeln('***** Current Weather Conditions *****');
  Writeln('The Temperature is ' + Format('%1.5g', [Temperature]) + ' Degrees F,');
  Writeln('Humidity is at ' + Format('%1.5g', [Humidity]) + '% and');
  Writeln('the Pressure is currently ' + Format('%1.5g', [Pressure]) + '" Hg.');
  Writeln('*********** End of Report ************');
  WriteLn;
end;

procedure TCurrentConditions.Update(Temp: Single; Humid: Single; Press: Single);
begin
  Temperature := Temp;
  Humidity := Humid;
  Pressure := Press;
  Display;
end;

end.

Forecast Display

unit ForecastDisplay;

interface

uses SysUtils, uObserver, WeatherStation;

type
  TForecastConditions = class(TDisplayObserver)
  private
    CurPressure: single;
    LastPressure: single;
  public
    constructor Create(Source:TWeatherStation);
    procedure Update(Temp: Single; Humid: Single; Press: Single); override;
    procedure Display; override;
  end;

implementation

constructor TForecastConditions.Create(Source:TWeatherStation);
begin
  inherited Create;
  Source.RegisterObserver(Self);
  CurPressure := 29.92;
end;

procedure TForecastConditions.Display;
begin
  WriteLn;
  Writeln('***** Current Weather Forecast *****');
  if CurPressure = LastPressure then
    Writeln('More of the same.')
  else if CurPressure > LastPressure then
    Writeln('Improving weather on the way!')
  else
    Writeln('Watch out for cooler, rainy weather.');
  Writeln('*********** End of Report ************');
  WriteLn;
end;

procedure TForecastConditions.Update(Temp, Humid, Press: Single);
begin
  LastPressure := CurPressure;
  CurPressure := Press;
  Display;
end;

end.

Statistics Display

I skipped this one. Accurately displaying float values is impossible for a project of such miniscule scope – I looked into it, extensively, and chucked it in as a hopeless muddle.  Actually it’s quite frustrating not being able to store 12.3 and then be able to simply display it as 12.3 using FloatToStr() instead of the 12.2999995499 gibberish you get, without calling in the ace coder to design a special module for it.  Math/Statistics with this mess ... not even going to go there.  I’ll play some more with the available tools and see where it takes me ...
Edit: Thanks to a comment left by “msohn” I see where I went wrong with using Format() and could, at some point, come back and add this Display.  Sorry, but Format() just isn’t something I’ve had a need to use up to recently.  I did fix the project code.

Heat Index Display

unit HeatIndexDisplay;

interface

uses SysUtils, uObserver, WeatherStation;

type
  THeatIndex = class(TDisplayObserver)
  private
    t: single;
    rh: single;
  public
    constructor Create(Source:TWeatherStation);
    procedure Update(Temp: Single; Humid: Single; Press: Single); override;
    procedure Display; override;
  end;

implementation

constructor THeatIndex.Create(Source:TWeatherStation);
begin
  inherited Create;
  Source.RegisterObserver(Self);
end;

procedure THeatIndex.Display;
var
  hi: single;
begin
  if t >= 70 then
    hi := 16.922999999999998 + 0.185212 * t +
      5.37941 * rh -
      (0.100254 * t * rh) +
      0.00941695 * t * t +
      0.00728898 * rh * rh +
      0.000345372 * t * t * rh -
      (0.000814971 * t * rh * rh) +
      1.02102E-005 * t * t * rh * rh -
      (3.8646E-005 * t * t * t) +
      2.91583E-005 * rh * rh * rh +
      1.42721E-006 * t * t * t * rh +
      1.97483E-007 * t * rh * rh * rh -
      (2.18429E-008 * t * t * t * rh * rh) +
      8.43296E-010 * t * t * rh * rh * rh -
      (4.81975E-011 * t * t * t * rh * rh * rh)
  else
    hi := 0;
  WriteLn;
  Writeln('********* Current Heat Index *********');

  if hi > 0 then
    Writeln('The Heat Index is ' + Format('%f', [hi]) + ' Degrees F.')
  else
    Writeln('Temp is less than 70F, the Heat Index is irrelevant.');

  Writeln('*********** End of Report ************');
  WriteLn;
end;

procedure THeatIndex.Update(Temp, Humid, Press: Single);
begin
  t := Temp;
  rh := Humid;
  Display;
end;

end.
Finally, pull it all together with ...

The Push Weather Program

program PushWeather;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  uObserver in '..\Common\uObserver.pas',
  uObservable in '..\Common\uObservable.pas',
  WeatherStation in 'WeatherStation.pas',
  CurCondDisplay in 'CurCondDisplay.pas',
  ForecastDisplay in 'ForecastDisplay.pas',
  HeatIndexDisplay in 'HeatIndexDisplay.pas';

var
  MyStation: TWeatherStation;
  CurCondDisp: TCurrentConditions;
  ForecastDisp: TForecastConditions;
  HeatIndexDisp: THeatIndex;

begin
  //ReportMemoryLeaksOnShutdown := DebugHook <> 0;

  try
    MyStation := TWeatherStation.Create;

    CurCondDisp := TCurrentConditions.Create(MyStation);
    ForecastDisp := TForecastConditions.Create(MyStation);
    HeatIndexDisp := THeatIndex.Create(MyStation);

    MyStation.SetMeasurements;

    Writeln('Let''s boot the Forecast Display and try again.');
    MyStation.RemoveObserver(ForecastDisp);

    MyStation.NotifyObservers;

    WriteLn('Press Enter to Exit.');
    Readln;

  finally
    HeatIndexDisp.Free;
    ForecastDisp.Free;
    CurCondDisp.Free;
    MyStation.Free;
  end;

end.
and, we’re done :)
Any comments you may have on either the code I’ve written as my interpretation of the Java code supplied in the book or the information I’ve provided would be most appreciated and well accepted.
Thanks for stopping by [and here’s hoping I got it close to right] ...
Dave

4 comments:

  1. A nice way of getting a float of any dimension into a readable string is Format() with '%1.5g'. Gives you 5 significant digits and might switch to exponential format when needed.

    ReplyDelete
  2. msohn, that gives me exactly the results I was looking for. I'll have to do a bit more study on Format() there seems to be a thing or two that I've missed. Having said that, it's quite a complex and powerful function.

    Thank-you.

    ReplyDelete
  3. It might be better to use interfaces IObservable and IObserver instead of base classes. Delphi supports no multiple inheritance, so if you want to use your Observer pattern implementation in existing classes that already derive from a base class, interfaces are the only way to go.

    ReplyDelete
  4. Hiya smasher, I totally agree but the compound classes work nicely and the pattern is just a pattern.

    ReplyDelete