This time we will discuss about factory method pattern and
its implementation with Delphi.
Factory Method Pattern (FMP)
Factory Method Pattern (FMP) is one of the basic design patterns as introduced by the Gang of Four (GoF) in their book Design Patterns. Design pattern is general reusable solution to commonly occuring problem in software design, mostly only applicable within OO environment.
FMP deals with creating objects (products) without specifying the exact class of object that will be created. This solution allows easy way to extend capability of the system to support new classes without the need to touch existing codes. Since we don't have to touch existing code, we know that there is no way we could introduce bugs to the existing system (the new bugs is only in the new system, so it's easier to trace).
Let's use our demo program (Image Viewer) for example. Image Viewer is our software to read a file, and if the format is supported it will render the content to the screen. When we start writing, due to time limitation, we decided to support only bitmap and jpg files. However we know that somewhere in the future we will have to support PNG images. And not only that, maybe we will need to support other image formats as well or even other format that could be rendered as image. Therefore we need to introduce a FMP class that recognizes a file then spits out proper class/object to read and render the file.
FMP class requirements
To implement a class of FMP we need to know its requirements.
Factory Method Pattern (FMP)
Factory Method Pattern (FMP) is one of the basic design patterns as introduced by the Gang of Four (GoF) in their book Design Patterns. Design pattern is general reusable solution to commonly occuring problem in software design, mostly only applicable within OO environment.
FMP deals with creating objects (products) without specifying the exact class of object that will be created. This solution allows easy way to extend capability of the system to support new classes without the need to touch existing codes. Since we don't have to touch existing code, we know that there is no way we could introduce bugs to the existing system (the new bugs is only in the new system, so it's easier to trace).
Let's use our demo program (Image Viewer) for example. Image Viewer is our software to read a file, and if the format is supported it will render the content to the screen. When we start writing, due to time limitation, we decided to support only bitmap and jpg files. However we know that somewhere in the future we will have to support PNG images. And not only that, maybe we will need to support other image formats as well or even other format that could be rendered as image. Therefore we need to introduce a FMP class that recognizes a file then spits out proper class/object to read and render the file.
FMP class requirements
To implement a class of FMP we need to know its requirements.
- FMP class must know the base class of the products. So for the Image Viewer we have to specify the base class to handle the graphic formats. Fortunately Delphi already provide perfect candidate for this, we don't have to write new class. The base class that we will use is TGraphic, which defined in Graphics unit.
- FMP class must be feed with initial data to produce proper product. In Image Viewer we will feed the image file name to the FMP class. The FMP will use the extension part from the file name for its decision. Yes, it's not foolproof. But I think it's suffice for our demo. Detecting image format from the actual content of the file is a topic for another article.
- FMP class accepts registration of new class (derived from the base class) to handle particular image format. This actually an extension of basic FMP specification, but the answer of this requirement allows to add new format with true 0 touching existing code.
Based on the above requirements, now we can write our skeleton FMP class. Let's name our FMP class TImageHandlers.
Code:
uses
Graphics
;
type
TImageRenderers=class
public
function GetImageRenderer(const AFilename: string; var ARenderer: TGraphic): Boolean;
function GetImageRendererClass(const AFileName: string; var ARendererClass: TGraphicClass): Boolean;
procedure RegisterImageRendererClass(const AExt: string; ARendererClass: TGraphicClass);
end;
function
TImageRenderers.GetImageRenderer
This method queries internal collection of renderer class to find one associated with extention part of the AFilename. If no renderer class found, this method returns false, otherwise it returns true with an instance of the renderer class passed in ARenderer parameter.
function TImageRenderers.GetImageRendererClass
Similar with method GetImageRenderer, but this one pass the renderer class instead of an instance. Useful when the caller wants to instantiate many renderer object of the same image format.
procedure TImageRenderers.RegisterImageRendererClass
This method registers new renderer class that capable to render images with format associated with the extension AExt.
Internal Collection of Renderer Class
From previous notes, we know that we need to implement an internal collection of renderer class. This collection must store association data of extensions and corresponding renderer class. Well, if you have enough time playing with Delphi before you will immediately know the best candidate for this collection. Yes, a descendant of TStrings, i.e. TStringList is the perfect candidate. As the name implies, TStrings main purpose is to hold many strings, and each string can be accompanied by a reference to a TObject. For our purpose, we can typecast that TObject reference into TGraphicClass to get it to store reference to our renderer class. Furthermore, TStringList provides ways to automatically sort the strings and to make sure the strings it contains are uniques.
Now, let's update our class along with implementation code.
This method queries internal collection of renderer class to find one associated with extention part of the AFilename. If no renderer class found, this method returns false, otherwise it returns true with an instance of the renderer class passed in ARenderer parameter.
function TImageRenderers.GetImageRendererClass
Similar with method GetImageRenderer, but this one pass the renderer class instead of an instance. Useful when the caller wants to instantiate many renderer object of the same image format.
procedure TImageRenderers.RegisterImageRendererClass
This method registers new renderer class that capable to render images with format associated with the extension AExt.
Internal Collection of Renderer Class
From previous notes, we know that we need to implement an internal collection of renderer class. This collection must store association data of extensions and corresponding renderer class. Well, if you have enough time playing with Delphi before you will immediately know the best candidate for this collection. Yes, a descendant of TStrings, i.e. TStringList is the perfect candidate. As the name implies, TStrings main purpose is to hold many strings, and each string can be accompanied by a reference to a TObject. For our purpose, we can typecast that TObject reference into TGraphicClass to get it to store reference to our renderer class. Furthermore, TStringList provides ways to automatically sort the strings and to make sure the strings it contains are uniques.
Now, let's update our class along with implementation code.
Code:
unit FMP;
interface
uses
Graphics
, Classes
;
type
TImageRenderers=class
private
FClasses: TStringList;
public
constructor Create;
destructor Destroy; override;
function GetImageRenderer(const AFilename: string; var AImageRenderer: TGraphic): Boolean;
function GetImageRendererClass(const AFilename: string; var AImageRendererClass: TGraphicClass): Boolean;
procedure RegisterImageRendererClass(const AExt: string; AImageRendererClass: TGraphicClass);
end;
implementation
uses
SysUtils
;
{ TImageRenderers }
constructor TImageRenderers.Create;
begin
FClasses := TStringList.Create;
FClasses.Duplicates := dupIgnore;
end;
destructor TImageRenderers.Destroy;
begin
FClasses.Free;
inherited;
end;
function TImageRenderers.GetImageRenderer(const AFilename: string;
var AImageRenderer: TGraphic): Boolean;
var
vImgRendererClass: TGraphicClass;
begin
// get the image renderer class using "GetImageRendererClass" method
Result := GetImageRendererClass(AFilename, vImgRendererClass);
if Result then
// create an instance of the image renderer class and pass it in
// AImageRenderer parameter
AImageRenderer := vImgRendererClass.Create;
end;
function TImageRenderers.GetImageRendererClass(const AFilename: string;
var AImageRendererClass: TGraphicClass): Boolean;
var
vExtIndex: Integer;
vExt : string;
begin
// get the file extension of the given file name
vExt := LowerCase(ExtractFileExt(AFilename));
// see if the extension is "registered"
vExtIndex := FClasses.IndexOf(vExt);
Result := vExtIndex > -1; // vExtIndex with the value lower than 0 means the
// extension is not "registered", else the
// extension is "registered"
if Result then
// get the class from the extension index
AImageRendererClass := TGraphicClass(FClasses.Objects[vExtIndex]);
end;
procedure TImageRenderers.RegisterImageRendererClass(const AExt: string;
AImageRendererClass: TGraphicClass);
var
vExtIndex: Integer;
begin
// Find out if the extension has been registered or not
vExtIndex := FClasses.IndexOf(AExt);
if vExtIndex < 0 then
// the extension is not registered, just add extension along with the
// renderer class
FClasses.AddObject(AExt, TObject(AImageRendererClass))
else
// the extension is already registered, update the renderer class (here we
// assume that newer renderer is better, and newer renderers registered
// later than the older ones)
FClasses.Objects[vExtIndex] := TObject(AImageRendererClass);
end;
end.
Now we are half way there.
There are some issues we haven't addressed. First, we haven't provided the way
to expose our FMP class globally. We need to expose it globally so anywhere in
our project we only use the same instance of our FMP class. In design patterns
this requirement is solved by Singleton Pattern. Thus that's how we
solve this problem, by providing a single instance of our FMP class in a global
variable. This instance will be created and destroyed automatically. So we need
to the following lines just before the implementation keyword (to make
the variable global).
Code:
var
ImageRenderers: TImageRenderers;
And add the following lines
just before the final end (then end which ended by a period).
Code:
initialization
// create the singleton instance of TImageRenderers
ImageRenderers := TImageRenderers.Create;
finalization
// destroy the singleton instance of TImageRenderers
ImageRenderers.Free;
Note that the singleton pattern
implemented here is just a simple or weak implementation. There are
stronger/stricter ways to implement singleton with Delphi, but that's another topic.
Now the second part that we have to address is the registration of basic image formats. Remember that in first version we have to support bitmap and jpg files. Fortunately Delphi already provided TGraphic descendant that capable to handle bitmap and JPEG image. Their names are TBitmap and TJPEGImage. Note that to use TJPEGImage you have to add Jpeg unit in your uses list.
So now we must add Jpeg in our uses list and add the following lines to register our basic image format renderers in the initialization block just after we created the singleton instance of TImageRenderers.
Now the second part that we have to address is the registration of basic image formats. Remember that in first version we have to support bitmap and jpg files. Fortunately Delphi already provided TGraphic descendant that capable to handle bitmap and JPEG image. Their names are TBitmap and TJPEGImage. Note that to use TJPEGImage you have to add Jpeg unit in your uses list.
So now we must add Jpeg in our uses list and add the following lines to register our basic image format renderers in the initialization block just after we created the singleton instance of TImageRenderers.
Code:
ImageRenderers.RegisterImageRendererClass('.bmp', TBitmap);
ImageRenderers.RegisterImageRendererClass('.jpg', TJPEGImage);
ImageRenderers.RegisterImageRendererClass('.jpeg', TJPEGImage);
ImageRenderers.RegisterImageRendererClass('.jp', TJPEGImage);
Now our codes for image renderer factory has completed. It's
time to deal with actual rendering or the GUI part.
No comments:
Post a Comment