2005. július 4., hétfő

Reducing Source Code Complexity in your application


Problem/Question/Abstract:

Have you ever written an application where things have to know when things happen, such as when an object gets freed then you need to update some UI screen or remove some depency. Or in the case of a paint program where when a mode change requires a cursor change, buttons to enable or disable or push down... if something gets deleted then you have to do this and that etc... I have a solution that will keep your code clean of linking code.

Answer:

There are times when you write an application that turns into a linking nightmare when your system needs to react to certain conditions.  Examples are Mode changing in a paint program requires cursor changes, an object being updated needs to update some UI element or disable and enable controls, when an object gets freed you need to remove dependencies.  In other words there are side effects that you need to happen as a result of something changing in your application. Coding these side effects can produce some nasty code that is like a big spider web.

The solution to the problem is to use a "Message Center". I have created a easy to use MessageCenter class that uses the built in messaging capablity already built into TObject.  Source code is at the end of this artical.

1. Concept of the message center

The concept is simple, you have a central "hub" that receives maybe all actions that happen in your program.  Certain parts of your program need to change when these events happen.  Instead of hard coding these "reactions" into your code, you send the message of the event to the message center in a record structure.  Anything that needs to react or change based on the event is registered with and notified by the MessageCenter.

2. Example Implementation

This app is an image editor where you can have multiple images opened at once.
Each Image is opened in a Form class of TForm_ImageEdit.
A graphical list of buttons are listed at the top of the main form, there is one button per opened image and a picture of the image is drawn on the surface of the button.  Users can click the button and active the form for that image.

The rule of the system is
A button should be added when a new form is added.
The button should remove when the form is removed.
The button should push down when the editor form becomes active.

First define the MessageID and the record for the message.

const
  MID_ImageEdit = 14936;

type
  TMID_ImageEdit = packed record
    MessageID: Cardinal; // This is required field for Dispatching
    Action: (aDestroyed, aActivated);
    ImageEdit: TForm_ImageEdit;
  end;

Then within the TForm_ImageEdit Broadcast the messages...

procedure TForm_ImageEdit.FormDestroy(Sender: TObject);
var
  M: TMID_ImageEdit;
begin
  with M do
  begin
    M.MessageID := MID_ImageEdit;
    M.Action := aClosed;
    M.ImageEdit := Self;
  end;
  GetMessageCenter.BroadcastMessage(Self, M);
end;

procedure TForm_ImageEdit.FormActivate(Sender: TObject);
var
  M: TMID_ImageEdit;
begin
  with M do
  begin
    M.MessageID := MID_ImageEdit;
    M.Action := aActivated;
    M.ImageEdit := Self;
  end;
  GetMessageCenter.BroadcastMessage(Self, M);
end;

Now to edit the main form

At some point in your main form when you create the Image Editor, add this code after creation:

F := TForm_ImageEdit.Create(Self);
// Listen to messages
GetMessageCenter.AttachListner(Self, F);

// Next few lines will add the button for the new form at the top of the main window.
{.
.
. }

This way the Main form will receive messages from the ImageEditor window.

So now Add this MessageHandler to your main form:
Create this method to receive messages of type MID_IMageEdit:

procedure ImageEditorWindowChanged(var Msg: TMID_ImageEdit); message MID_ImageEdit;

And implement it in this way

procedure TForm_NMLDA.ImageEditorWindowChanged(var Msg: TMID_ImageEdit);
begin
  case Msg.Action of
    aDestroyed:
      begin
        ImageEditorClosed(Msg.ImageEdit);
        GetMessageCenter.DetachListner(Self, Msg.ImageEdit);
      end;
    aActivated: EditorFocused(Msg.ImageEdit);
  end;
end;

ImageEditorClosed method will remove the button from the main form EditorFocused will push down the button associated with the ImageEditor.

Thats all, you have low coupling and you may attach as many listners as you like.

This concept has a lot of potential and it will make your complex apps very simple and maintainable.

Here is the code:

unit MessageCenter;
{
  William Egge public@eggcentric.com
  Created Feb - 28, 2002
  You can modify this code however you wish and use it in commercial apps.  But
    it would be cool if you told me if you decided to use this code in an app.

  The goal is to provide an easy way to handle notifications between objects
  in your system without messy coding.  The goal was to keep coding to a minimum
  to accomplish this. That is why I chose to use Delphi's built in
  Message dispatching.
  This unit/class is intended to be a central spot for messages to get dispatched,
    every object in the system can use the global GetMessageCenter function.
  You may also create your own isolated MessageCenter by creating your own
    instance of TMessageCenter.. for example if you had a large subsystem and
    you feel it would be more effecient to have its own message center.

  The goal is to capture messages from certain "Source" objects.

  Doc:
    procedure BroadcastMessage(MessageSource: TObject; var Message);
      The message "Message" will be sent to all objects who called AttachListner
      for the MessageSource.
      If no objects have ever called AttachListner then nothing will happen and
      the code will not blow up :-).  Notice that there is no registration for
      a MessageSource, this is because the MessageSource registration happens
      automatically when a listner registers itself for a sender.
      (keeping external code simpler)

    procedure AttachListner(Listner, MessageSource: TObject);
      This simply tells the MessageCenter that you want to receive messages from
      MessageSource.

    procedure DetachListner(Listner, MessageSource: TObject);
      This removes the Listner so it does not receive messages from MessageSource.

  Technique for usage with interfaces:
    If your program is interface based then its not possible to pass a
    MessageSource but it IS possible to pass an object listner if it is being
    done from within the object wanting to "listen" (using "self").
    To solve the problem of not being able to pass a MessageSource, you can
    add 2 methods to your Sender interface definition,
    AttachListner(Listner: TObject) and DetachListner(Listner: TObject).
    Internally within those methods your interfaced object can call the
    MessageCenter and pass its object pointer "Self".

  Info:
    Performance and speed were #1 so...

    MessageSources are sorted and are searched using a binary search so that
    a higher number of MessageSources should not really effect runtime performance.
    The only performance penalty for this is on adding a new MessageSource because
    it has to do an insert rather than an add, this causes all memory to be shifted
    to make room for the new element.  The benifit is fast message dispatching.

    There is no check for duplicate MesssageListners per Sender, this would have
    slowed things down and this coding is usefull only when you have bugs.  And
    hoping you prevent bugs, you do not have to pay for this penalty when your
    code has no bugs.
}

interface
uses
  Classes, SysUtils;

type
  TMessageCenter = class
  private
    FSenders: TList;
    FBroadcastBuffers: TList;
    function FindSenderList(Sender: TObject; var Index: Integer): TList;
  public
    constructor Create;
    destructor Destroy; override;
    procedure BroadcastMessage(MessageSource: TObject; var Message);
    procedure AttachListner(Listner, MessageSource: TObject);
    procedure DetachListner(Listner, MessageSource: TObject);
  end;

  // Shared for the entire application
function GetMessageCenter: TMessageCenter;

implementation
var
  GMessageCenter: TMessageCenter;
  ShuttingDown: Boolean = False;

function GetMessageCenter: TMessageCenter;
begin
  if GMessageCenter = nil then
  begin
    if ShuttingDown then
      raise
        Exception.Create('Shutting down, do not call GetMessageCenter during shutdown.');
    GMessageCenter := TMessageCenter.Create;
  end;

  Result := GMessageCenter;
end;

{ TMessageCenter }

procedure TMessageCenter.AttachListner(Listner, MessageSource: TObject);
var
  L: TList;
  Index: Integer;
begin
  L := FindSenderList(MessageSource, Index);
  if L = nil then
  begin
    L := TList.Create;
    L.Add(MessageSource);
    L.Add(Listner);
    FSenders.Insert(Index, L);
  end
  else
    L.Add(Listner);
end;

procedure TMessageCenter.BroadcastMessage(MessageSource: TObject; var Message);
var
  L, Buffer: TList;
  I: Integer;
  Index: Integer;
  Obj: TObject;
begin
  L := FindSenderList(MessageSource, Index);
  if L <> nil then
  begin
    // Use a buffer because objects may detach or add during the broadcast
    // Broadcast can be recursive.  Only broadcast to objects that existed
    // before the broadcast and not new added ones.  But do not broadcast to
    // objects that are deleted during a broadcast.
    Buffer := TList.Create;
    try
      FBroadcastBuffers.Add(Buffer);
      try
        for I := 0 to L.Count - 1 do
          Buffer.Add(L[I]);

        // skip 1st element because it is the MessageSender
        for I := 1 to Buffer.Count - 1 do
        begin
          Obj := Buffer[I];
          // Check for nil because items in the buffer are set to nil when they are removed
          if Obj <> nil then
            Obj.Dispatch(Message);
        end;
      finally
        FBroadcastBuffers.Delete(FBroadcastBuffers.Count - 1);
      end;
    finally
      Buffer.Free;
    end;
  end;
end;

constructor TMessageCenter.Create;
begin
  inherited;
  FSenders := TList.Create;
  FBroadcastBuffers := TList.Create;
end;

destructor TMessageCenter.Destroy;
var
  I: Integer;
begin
  for I := 0 to FSenders.Count - 1 do
    TList(FSenders[I]).Free;
  FSenders.Free;
  FBroadcastBuffers.Free;
  inherited;
end;

procedure TMessageCenter.DetachListner(Listner, MessageSource: TObject);
var
  L: TList;
  I, J: Integer;
  Index: Integer;
begin
  L := FindSenderList(MessageSource, Index);
  if L <> nil then
  begin
    for I := L.Count - 1 downto 1 do
      if L[I] = Listner then
        L.Delete(I);

    if L.Count = 1 then
    begin
      FSenders.Remove(L);
      L.Free;
    end;

    // Remove from Broadcast buffers
    for I := 0 to FBroadcastBuffers.Count - 1 do
    begin
      L := FBroadcastBuffers[I];
      if L[0] = MessageSource then
        for J := 1 to L.Count - 1 do
          if L[J] = Listner then
            L[J] := nil;
    end;
  end;
end;

function TMessageCenter.FindSenderList(Sender: TObject;
  var Index: Integer): TList;
  function ComparePointers(P1, P2: Pointer): Integer;
  begin
    if LongWord(P1) < LongWord(P2) then
      Result := -1
    else if LongWord(P1) > LongWord(P2) then
      Result := 1
    else
      Result := 0;
  end;
var
  L, H, I, C: Integer;
begin
  Result := nil;
  L := 0;
  H := FSenders.Count - 1;
  while L <= H do
  begin
    I := (L + H) shr 1;
    C := ComparePointers(TList(FSenders[I])[0], Sender);
    if C < 0 then
      L := I + 1
    else
    begin
      H := I - 1;
      if C = 0 then
      begin
        Result := FSenders[I];
        L := I;
      end;
    end;
  end;
  Index := L;
end;

initialization
finalization
  ShuttingDown := True;
  FreeAndNil(GMessageCenter);

end.


Component Download: http://www.eggcentric.com/download/MCDemo.zip

Nincsenek megjegyzések:

Megjegyzés küldése