Jump to content

Factory Design Pattern With Delphi (Image Viewer demo project)

- - - - -

This topic has been archived. This means that you cannot reply to this topic.
1 reply to this topic

#1
LuthfiHakim

LuthfiHakim

    Programming God

  • Members
  • PipPipPipPipPipPipPip
  • 765 posts
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.
  • 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.

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.


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).


var

  ImageRenderers: TImageRenderers;


And add the following lines just before the final end (then end which ended by a period).


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.


  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 (continued in next post).

Edited by LuthfiHakim, 19 November 2010 - 10:24 AM.


#2
LuthfiHakim

LuthfiHakim

    Programming God

  • Members
  • PipPipPipPipPipPipPip
  • 765 posts
For the GUI please do the following steps of preparation.
  • Create new project
  • Add our FMP unit to the project
  • Make sure your main form use FMP unit (Alt+F - Use Unit, or Alt+F11)
  • Drop a TImage in the main form, adjust its Align property into alClient to cover the whole form. Also set AutoSize and Center to false.
  • Drop a TOpenPictureDialog (from Dialogs tab)
  • Drop a TMainMenu, then double click the TMainMenu to open menu editor. With the menu editor and a File menu, and under it add a menu item Open... Let's name the Open.. menu as mnOpen
  • Add PngImage unit in your interface uses list. This is for teasing only, since currently we don't support PNG. But we want to be able to select PNG files with our open picture dialog.

Attached File  OpenPicDlg001.png   14.1K   107 downloads

Now time to provide users a way to choose an image file and then render it for them, if we capable to. To do so, add the following code for the event OnClick of mnOpen (you can just double click the menu item in menu editor to get skeleton code).

procedure TForm1.mnOpenClick(Sender: TObject);
var
  vImage: TGraphic;
begin
  if not OpenPictureDialog1.Execute then Exit;
  if ImageRenderers.GetImageRenderer(OpenPictureDialog1.FileName, vImage) then
  try
	vImage.LoadFromFile(OpenPictureDialog1.FileName);
	Image1.Picture.Graphic := vImage;
  finally
	vImage.Free;
  end;
end;

That's it! Our first version of Image Viewer is completed! You can run the project and then select jpg or bmp files to view their content. Since we did not alter filter property of our TOpenPictureDialog you still can see files with formats we don't support (e.g. png, icon, wmf and emf). But note that there is no error generated when we try to open unsupported files, since our FMP class properly know it.

Attached File  Project001.png   18.26K   254 downloads

Add PNG Format Support
Now it's time to add support for PNG format. We have to do it in a hurry, since a few thousands of angry users are waiting in front of your house, waiting for new Image Viewer that supports PNG. :)

To add new product to our factory you only need to fulfill two requirements.
  • You must have a class that handles new feature/capability, and this class must be based from our product base class. In our case we need to support PNG Image, and there is a fine PNG library for Delphi that we can use. It's TPNGObject (Delphi 7) or TPngImage (Delphi 2010) defined in PNGImage library and this class was derived from TGraphic (our products' base class).
  • Register the new product class to our FMP object (ImageRenderers). In our case just add the following code inside OnCreate event of our main form.
    		  ImageRenderers.RegisterImageRendererClass('.png', TPNGObject);
    		

You can run the project and now you can happily view PNG files.

See? In our case the addition of PNG image support was done in less than 5 seconds (if you already have PNGImage library properly installed in your Delphi environment). And there is no existing code we need to touch.

Attached File  ViewPngImg001.png   13.17K   230 downloads

Complete source code is attached. Feel free to use the code or expand the code, no string attached. But it's very appreciated if you could drop me a message telling what are you using it for.

Attached Files