Monday, 27 October 2008

MVC in Delphi (Part I)

We have a large Delphi software project at work, that has been in development for the last 10 years and is now basically unmaintainable. It uses threads to create conversations with an iSeries I5 (an AS/400 in old money). These threads are unmanaged, interdependant and duplicated throughout the application.

We have been on the point of refactoring many times, but have not had the time or the will-power to actually do it. So, here goes ...

MVC

Basically the MVC pattern looks closest to what I need. A data domain, and dependant views on it, so it "... isolates business logic from user interface considerations..."

The parts of the pattern are ..











Model


This is basically an Observable ...


IObservable = interface

procedure RegisterObserver (obs : IObserver);
procedure UnregisterObserver (obs : IObserver);

procedure Notify (const obj : TObject);
procedure ClearObservers ();

procedure DoCommand (cmd : TCommand);

end;


A default implementation of IObservable is the TObservable class, from which more concrete implementations should descend.


///This is the default implementation of IObservable
///IObservable
TObservable = class (TInterfacedObject,
IObservable)
private
FObservers : TClassList;
FCurrent : TObject;

public
constructor Create;
destructor Destroy; override;

procedure SetCurrent (obj: TObject);

procedure RegisterObserver (obs : IObserver);
procedure UnregisterObserver (obs : IObserver);
procedure ClearObservers ();
procedure Notify (const obj : TObject);

procedure DoCommand (cmd : TCommand); virtual;

property Current : TObject read FCurrent write FCurrent;

end;


View

This is basically an Observer ...


IObserver = interface

procedure UpdateView (obj : TObject);

end;

The IObserver and IObservable pattern is a standard pattern from Design Patterns: Elements of Reusable Object-Oriented Software. It is half way there, with the Observable registering all of the Observer objects that are interested in when it changes. However, there is not a way of interacting with the Observer, in order to make it carry out operations, this is where the Controller comes in.

Controller

The controller is used (in my code at least) register all of the Observers with the Observables, and to marshal commands from the View to the Model. My interface for a Controller looks like this;


IController = interface

function GetModel : IObservable;

procedure SetModel (const m : IObservable);

function GetView (index: integer): IObserver;

procedure SetView (index: integer; const v : IObserver);

property Model : IObservable read GetModel write SetModel;

property Views[index: integer]: IObserver read GetView write SetView;

procedure DoCommand (cmd : TCommand);

procedure AddView (v : IObserver);

end;


This maintains a list of Views (accessed via the Views property), and the Model which they are interested in. The commands are processed via the DoCommand method (this is a bit of a hack I think, and I can't quite think of a way to do this nicely!).

Concrete Example

So the interfaces above describe the basic building blocks of an MVC interface. Lets try and build a concrete example to see how it works.

My example model will be a single extended value, which will be initialised at startup by the Model, and then operations can be carried out on it by the View, via the Controller.


First we define the concrete model ...


///A small implementation of the MVC architecture
TDomainModel = class (TObservable)
private
/// Underlying data object
FData : extended;

public
///Property that represents the underlying data
property Data : extended read FData write FData;

///Executes the given command
procedure DoCommand (cmd : TCommand); override;

///Creates the underlying data
constructor Create;

///Function that is passed to the CommandThread for execution
///TCommandThread
procedure ExecuteCommand (cmd : TCommand);
end;


As can be seen, we descend from the base class
TObservable, which implements all of the IObservable methods, and provides some basic functionality needed by all TObservable objects (see above).

The DoCommand method wraps up the TCommand object (more of that later), and then spawns off a thread to actually do the processing. This hides all of the gubbins of getting data and processing it from the Controller and the View. In the real example this would get stuff from the i5 via the conversation. It would mean that this method of using threads could be changed to use a managed queue, or whatever, without changing the View or the Controller, as they don't need to know HOW the data is fetched, just that it is ready.

In the example below, we just do some very simple maths


procedure TDomainModel.DoCommand (cmd : TCommand);
var
T : TCommandThread;

begin
t := TCommandThread.Create(true);

t.Cmd := cmd;
t.ParamList := cmd.ParamList;
t.Operation := ExecuteCommand;

t.Resume;
end;

procedure TDomainModel.ExecuteCommand (cmd : TCommand);
begin
if cmd.Command = 'HALF' then
begin
FData := FData / 2;
end;

if cmd.Command = 'MULTIPLY' then
begin
FData := FData * 2;
end;

if cmd.Command = 'SET' then
begin
assert (cmd.ParamCount <> -1, 'SET requires a parameter value');
FData := cmd.Params[0].Value;
end;

if cmd.Command = 'ADD' then
begin
assert (cmd.ParamCount <> -1, 'ADD requires a parameter value');
FData := FData + cmd.Params[0].Value;
end;

if cmd.Command = 'ZERO' then
begin
FData := 0.0;
end;

Notify(self);
end;

constructor TDomainModel.Create;
begin
FData := 10.0;
end;


I'll finish off this a bit later, and upload the source for the example

1 comment:

juncanonigo said...

this is cool... i'm a java programmer and i'm interested on how to do mvc stuffs here in delphi...