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.

10 comments:

  1. Very interesting system, though the "call ClassName in the initialization section" hack, and the explanation for it, made me cringe a little. I looked at that and said "please tell me he is not iterating over all declared types with a TRttiContext, searching each one for attributes." Then I looked at the code in SVN and... yep! That's exactly what's happening here. :(

    Here's an alternate idea: Since being required to put one entry per class in the initialization section is equivalent to registration, why not require actual registration? This would make it explicit what's going on, and it would also save a fair bit of time (on larger projects at least) when the catalog is being set up.

    ReplyDelete
  2. Another thing to consider: You can't assume that all relevant information will be available at the moment when TRttiCatalog.Create runs. There could easily be something in a plugin BPL library that gets loaded later. This makes a registration proc (and an unregistration proc!) essential for your framework.

    ReplyDelete
  3. I have not tested it in some larger projects since it's just some early prototype so I cannot say anything about the performance of looking through the entire RTTI at the start.
    I did not think about classes not being compiled into the application when they are not used anywhere when I originally found that library so that ClassName "hack" was just to make it work. I think I should open some QC entry about some compiler switch to not ignore unused types. Or someone points me into the right direction to make it work without that ugly workaround.
    One thing I was trying to avoid was the registering since then you have some dependency between the type you want to register and some kind of repository or catalog or at least to some register function. At the moment you just need to know the attributes. Also the whole point of defining the InheritedExport on some interface or abstract base class is lost when you have to register them manually. And in my opinion the possibility of declarative registering is one of the major benefits of MEF.
    I haven't looked if RTTI is capable of identifying types from certain modules. If it is that could solve the problem of plugin libraries loaded after the instantiation of the RTTI catalog.

    ReplyDelete
  4. The RTTI system can tell which module a type came from (take a look at TRttiPackage) but it doesn't have any notification for new packages being loaded. It checks when you ask for certain information (anything that calls TRttiPool.GetPackageList will make sure the package list is up to date) but it doesn't notify you that there's a new package. As far as I can tell, there's nothing in Delphi that allows you to setup an event handler to be executed when a module is loaded.

    I can understand wanting to reduce dependencies, but a certain amount of dependencies and coupling are necessary to make things actually work. There comes a point where you need to invoke Einstein on stuff like this: "Make everything as simple as possible, but not simpler." Doing away with registration and unregistration crosses that line and instead of reducing complexity, you start sweeping it under the carpet, and there it lurks in dark places, waiting to jump out and bite you in ways you don't expect. (Like trying to use the framework in packages that get loaded and unloaded independently of the startup/cleanup of your program.) Registration really is the only way I know of to get this right.

    ReplyDelete
  5. Even if it would be funny I don't want to be notified when a package is loaded since somewhere in my program I am responsible for loading them and I most likely know what modules I have loaded. It should be no problem to setup catalogs for those modules or add the exports from those modules to my existing catalog.
    I can also think of some kind of catalog sitting inside the library that grabs all the exports when said library is loaded. Then you can easily give that catalog to the container and use the new types.
    But I agree that it should be possible to remove the registered types from the catalog when modules are unloaded but even more important is to also remove existing objects that came from those modules - which is without a doubt much easier in some GC language.
    Anyway I don't claim this concept is working in all cases when you use libraries that get loaded/unloaded independently and I can repeat it, it's some early prototype and more of a proof of concept to show that Delphi is capable doing modern stuff - even if it's lacking behind .Net on many things.

    ReplyDelete
  6. Similar stuff :)
    http://www.delphi-forum.de/viewtopic.php?t=100779

    ReplyDelete
  7. @dph: Guess why I linked to your original post on DP. Also you never responded to my PM :)

    ReplyDelete
  8. Hi,
    From looking at the svn repository it looks like this approach is now deprecated. However, I like this approach - we use MEF in C# - and was wondering whether you have any information as to why when I load a package that has an object with the same interface (even if it is before the creation of the catalog and container), it is not loaded (even though I can see the initialization running)?

    ReplyDelete
  9. Using an own container is deprecated. But the MEF concept is not - since I am using the Delphi Spring container now.

    Is your question about the package referring to DSharp? Can you give me an example because I am afraid I don't understand your question.

    ReplyDelete
  10. The big drawback with text literals in attributes is that there is no compile time checking any more. Same issue in MEF on .NET. A partial solution is to make them string constants, but still.

    ReplyDelete