Friday, November 9, 2012

Use Custom Events to remove dependencies

I have used Delphi a lot but until now not realized how powerful events are. During the last week I got an Aha experience when I realize what can be done. And how clean code can be written with events.

An event is the same a a function pointer for those that use for example C or C++. Delphi have a lot of predefined events. For example a button can have OnClick, OnExit etc. It's easy to use them as Delphi IDE helps you with that.

If you use custom events it can help you break dependencies  between classes. This make it easier to write tests for the code and refactor it.

An Example
This illustrate a form that create and show a dialog. This dialog may also be called from 2 other forms. In the dialog there are special cases to handle this.
First look at the old code before the change:


frmBook.pas
procedure TBookForm.OrderSearch(Sender: TObject);
var
  vForm: TFindParcelForm;
begin
  vForm := TFindParcelForm.Create(self);
  vForm.ShowModal;
end;

procedure TBookForm.OrderNew(Sender: TObject);
begin
  // Code to make new order
end;


frmFindParcel.pas
type  TFindParcelForm = class(TForm)     // Som declarations
end;

// Dependencies to owner
uses
  frmBook,
  frmPlan,
  frmOrg; 

procedure TFindParcelForm.MakeOrderClick(Sender: TObject);
begin
  // Block1 of code here

  if owner is TBookForm) then
    (Owner as TBookForm).OrderNew(self)
  else if Owner is TPlanForm then
    (Owner as TPlanForm).OrderNew(self)
  else if Owner is TOrgForm then
    (Owner as TOrgForm).OrderNew(self)    

  // Block2 of code here
end;

Let's now use the predefined event TNotifyEvent. It is defined in VCL Classes.pas:


TNotifyEvent = procedure(Sender: TObject) of object;

frmBook.pas
procedure TBookForm.OrderSearch(Sender: TObject);
var
  vForm: TFindParcelForm;
begin
  vForm := TFindParcelForm.Create(self);
  vForm.OnNewOrder := OrderNew;
  vForm.ShowModal;
end;

// Have the same signature as TNotifyEvent

procedure TBookForm.OrderNew(Sender: TObject);
begin
  // Code to make new order
end;

frmFindParcel.pas

type  TFindParcelForm = class(TForm)     // Some declarations  private    fNewOrder: TNotifyEvent;  public    property OnNewOrder: TNotifyEvent read fNewOrder write fNewOrder; 
end;

procedure TFindParcelForm.MakeOrderClick(Sender: TObject);
begin
  // Block1 of code here

  if Assigned(OnNewOrder) then
    OnNewOrder(Self);

  // Block2 of code here
end;

So what happened was that we have broken the dependency from FrmFindParcel.pas to the owners TBookForm, TPlanForm and TOrgForm. This means that it is now easier to test frmFindParcel as a separate unit. Method MakeOrderClick is now simplified and is not aware of the owner. If else is also gone.

But we don't need to stop here. With a small addition we can make own custom events as the blog title suggests.

type
  TSetOrder = procedure(aOrder: TOrder; aPrice: Double) of objects;
  TGetOrder = function(): TOrder of objects;

  TFindParcelForm = class(TForm)     // Some declarations  private    fNewOrder: TNotifyEvent;

    fOnSetOrder: TSetOrder;
    fOnGetOrder: TGetOrder;
public    
    property OnNewOrder: TNotifyEvent read fNewOrder write fNewOrder; 
    property OnSetOrder: TSetOrder read fOnSetOrder write fOnSetOrder;
    property OnGetOrder: TGetOrder read fOnGetOrder write fOnGetOrder;
end;

We added 2 new events OnSetOrder that is a procedure with 2 parameters. And OnGetOrder that just return an order.

Usage:

frmBook.pas
procedure TBookForm.OrderSearch(Sender: TObject);
var
  vForm: TFindParcelForm;
begin
  vForm := TFindParcelForm.Create(self);
  vForm.OnNewOrder := OrderNew;
  vForm.OnSetOrder := SetActiveOrder;
  vForm.OnGetOrder := GetActiveOrder;
  vForm.ShowModal;
end;

procedure TBookForm. SetActiveOrder(aOrder: TOrder; aPrice: Double);
begin
  // Code to set Active Order
end;

function TBookForm. GetActiveOrder: TOrder;
begin
  // Code return active Order
end;

// Have the same signature as TNotifyEvent
procedure TBookForm.OrderNew(Sender: TObject);
begin
  // Code to make new order
end;

frmFindParcel.pas


procedure TFindParcelForm.TestOrderClick(Sender: TObject);
var
  vOrder, vOrder2: TOrder;
begin
  // Block1 of code here

  vOrder := MakeOrder;

  if Assigned(OnSetOrder) then
    OnSetOrder(vOrder);

  if Assigned(OnGetOrder) then
    vOrder2 := OnGetOrder;


  // Block2 of code here
end;

So to break dependencies and make code more testable, own custom events are a powerful tool to accomplish that.