Saturday, May 14, 2011

Going beyond dependency injection with MEF for Delphi

You probably read Nicks recent posts about his experiences with the Delphi Spring Framework and especially with their DI Container. To be honest I actually don't know how far they go with it but Nick most likely will enlighten us soon.

As you may know I really like many concepts that we can find in .Net and here is another one: the Managed Extensibility Framework. Its core parts are basically a DI container and one catalog (or more) that contains information about exports and imports. So you can create classes that are not referenced directly by any other part of your application and define an export on them. But that alone would be useless because we actually want to use that class, don't we? So on the other side you can specify an import. Be it for the constructor of another class or its properties.

In the following I will show you how to use this concept in your Delphi application. Because I am very bad in finding examples I took a look around and found this nice example for MEF and I will use very similar examples.

Hello world

So, let's get started. We will create a console application that looks like this:
program MEFSample;

{$APPTYPE CONSOLE}

uses
  MEFSample.Main,
  MEFSample.Message,
  System.ComponentModel.Composition.Catalog,
  System.ComponentModel.Composition.Container,
  SysUtils;

var
  main: TMain;
  catalog: TRttiCatalog;
  container: TCompositionContainer;
begin
  ReportMemoryLeaksOnShutdown := True;
  main := TMain.Create;
  catalog := TRttiCatalog.Create();
  container := TCompositionContainer.Create(catalog);
  try
    try
      container.SatisfyImportsOnce(main);
      main.Run();
    except
      on E: Exception do
        Writeln(E.ClassName, ': ', E.Message);
    end;
  finally
    container.Free();
    catalog.Free();
    main.Free;
  end;
  Readln;
end.


We create the catalog which pulls all the exports and imports from the RTTI and pass it to the composition container which is responsible for resolving those informations when creating new objects or using the SatisfyImportsOnce method on an already existing object like in our example.

Our MEFSample.Main unit looks as follows:
unit MEFSample.Main;

interface

uses
  System.ComponentModel.Composition;

type
  TMain = class
  private
    FMsg: TObject;
  public
    procedure Run;

    [Import('Message')]
    property Msg: TObject read FMsg write FMsg;
  end;

implementation

procedure TMain.Run;
begin
  Writeln(Msg.ToString);
end;

end.

The unit System.ComponentModel.Composition contains all the attributes. If you do not include it, you get the "W1025 Unsupported language feature: 'custom attribute'" compiler warning which means those attributes are not applied.
We specify the Msg property as named import. When calling the SatifyImportOnce Method (which is also used internally when creating objects with the CompositionContainer) it looks for all those imports and tries to find matching exports.

That leads us to the last unit for this first example:
unit MEFSample.Message;

interface

uses
  System.ComponentModel.Composition;

type
  [Export('Message')]
  TSimpleHello = class
  public
    function ToString: string; override;
  end;

implementation

function TSimpleHello.ToString: string;
begin
  Result := 'Hello world!';
end;

initialization
  TSimpleHello.ClassName;

end.

Again, we need to use System.ComponentModel.Composition to make the Export attribute work. We have some simple class that is exported with a name.
One important point: Since this class is referenced nowhere else in our application the compiler will just ignore it. That is why we need to call some method of it in the initialization part of the unit which is called when the application starts. So this makes the compiler include our class.

When we start the application we see the amazing "Hello world".

Using contracts

This worked but actually that is not how we should do this. So let's define a contract called IMessage.
unit MEFSample.Contracts;

interface

uses
  System.ComponentModel.Composition;

type
  [InheritedExport]
  IMessage = interface
    ['{7B32CB2C-F93F-4C59-8A19-89D6F86F36F1}']
    function ToString: string;
  end;

implementation

end.
We use another attribute here which tells the catalog to export all classes, that implement this interface. We also need to specify a guid for that interface. We change our TSimpleHello class:
TSimpleHello = class(TInterfacedObject, IMessage)
and in our main class we change the Msg property:
[Import]
property Msg: IMessage read FMsg write FMsg;

The more the better!

What keeps us from creating another class that implements IMessage? Nothing, so let's do this:

type
  TSimpleHola = class(TInterfacedObject, IMessage)
  public
    function ToString: string; override;
  end;

implementation

function TSimpleHola.ToString: string;
begin
  Result := 'Hola mundo';
end;

initialization
  TSimpleHola.ClassName;

When we run this we get an ECompositionException with message 'There are multiple exports but a single import was requested.' which totally makes sense. So we need to change something:
[ImportMany]
property Msgs: TArray<IMessage> read FMsgs write FMsgs;
and the Run method:
procedure TMain.Run;
var
  m: IMessage;
begin
  for m in FMsgs do
    Writeln(m.ToString);
end;

We start the application and get both messages.

Breaking it down

What if we only want to export and import smaller parts than a whole class? Well then define the export on those parts!

type
  TSimpleHello = class(TInterfacedObject, IMessage)
  private
    FText: string;
  public
    function ToString: string; override;
    [Import('Text')]
    property Text: string read FText write FText;
  end;

  TTextProvider = class
  private
    function GetText: string;
  public
    [Export('Text')]
    property Text: string read GetText;
  end;

implementation

function TSimpleHello.ToString: string;
begin
  Result := FText;
end;

function TTextProvider.GetText: string;
begin
  Result := 'Bonjour tout le monde';
end;

initialization
  TSimpleHello.ClassName;
  TTextProvider.ClassName;

So what is this all about? Different parts of the applications can be created and put together in a declarative way using attributes. With MEF you can create code that is free of unnecessary dependencies - clean code.

The sample and the required units can be downloaded here or directly from the svn. The source is based on some implementation I originally found here.

P.S. I just made the sample also work in Delphi 2010 - this is available in svn.