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.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.
-- 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
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.
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
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.
ReplyDeletemsohn, 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.
ReplyDeleteThank-you.
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.
ReplyDeleteHiya smasher, I totally agree but the compound classes work nicely and the pattern is just a pattern.
ReplyDelete