MEF Teil 7 – Exportieren über eine Class Factory

Über die Attribute Import und Export werden Objekte fest miteinander verbunden, vorausgesetzt, sie sind zueinander kompatibel. Nicht immer ist diese ‘feste’ Bindung erwünscht. Eine Class Factory kann hierbei helfen, diese starre Zuordnung aufzubrechen. Der mögliche Einsatz einer Class Factory soll durch zwei einfache Beispiele gezeigt werden.

Ziel einer Class Factory ist es, Objekte zu erzeugen. Unterklassen entscheiden aber, welche Klasse genau instanziiert wird. Dazu liefert die Class Factory die passende Klasse in Abhängigkeiten von Informationen, die vom Client bereitgestellt werden oder erst zur Laufzeit ermittelt wurden.

Als Grundlage für die Beispiele soll ein Textlogger dienen, der über verschiedene Formatter den Ausgabetext unterschiedlich formatieren kann. In beiden Beispielen stellt die Klasse ConsoleLogger die Methode Log() bereit. Innerhalb der Klasse ConsoleLogger werden auf unterschiedliche Art und Weise Instanzen der Klasse FormatterBase (bzw. Ableitungen davon) erzeugt. Eine Ableitung (FormatterTimeStamp) ergänzt die Meldung mit der aktuellen Uhrzeit. Eine weitere Ableitung, die Klasse FormatterDateTimeStamp, gibt neben der Uhrzeit auch das aktuelle Datum aus.

Die Hauptanwendung, der Textlogger und der Formatter sollen über das Managed Extensibility Framework gebunden werden. Die eigentliche Implementierung des Formatters soll durch eine Class Factory flexibel erzeugt werden.

Beispiel 1

Der Host ist bei diesem Beispiel recht einfach aufgebaut. Eine Eigenschaft vom Typ ConsoleLogger importiert über das Managed Extensibility Framework die passende Instanz. Über die Methode Log() werden die Meldungen ausgegeben.

using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using Logger;

namespace Host
{
    class Program
    {
        [Import]
        public ConsoleLogger Logger { get; set; }

        static void Main(string[] args)
        {
            new Program().Run();
        }

        void Run()
        {
            var catalog = new DirectoryCatalog(".");
            var container = new CompositionContainer(catalog);
            container.ComposeParts(this);
            Logger.Log("Message A");
            Logger.Log("Message B");
            Logger.Log("Message C");
        }
    }
}

Die Klasse ConsoleLogger enthält die Methode Log() und die Eigenschaft FormatterFunc. FormatterFunc ist ein Delegate vom Typ Func<FormatterBase>, liefert also immer ein Objekt vom Typ FormatterBase zurück.

using System;
using System.ComponentModel.Composition;
using Formatter;

namespace Logger
{
    [Export]
    public class ConsoleLogger
    {
        [Import]
        private Func<FormatterBase> FormatterFunc { get; set; }

        public void Log(string message)
        {
            string formattedString = FormatterFunc().Format(message);
            Console.WriteLine(formattedString);
        }
    }
}

Exportiert wird die Methode aus der eigentlichen Class Factory. Die Methode GetTextFormatter() wurde hier mit dem Attribute Export versehen. Da diese kompatibel zu der Eigenschaft FormatterFunc ist, werden beide durch das Managed Extensibility Framework miteinander gebunden.

public class FormatterFactory
{
    private static bool toogle;

    [Export]
    public FormatterBase GetTextFormatter()
    {
        // an dieser Stelle wird entschieden, welcher
        // Formatter zurückgeliefert werden soll.
        toogle = !toogle;
        if (toogle)
            return new FormatterTimeStamp();
        else
            return new FormatterDateTimeStamp();
    }
}}

Die Methode GetTextFormatter() erzeugt beim Aufruf das gewünschte Objekt. Zu Testzwecken liefert diese Methode einfach abwechselnd zwei verschiedene Objekte zurück. Die Entscheidung, welches Objekt erzeugt wird, würde bei einer realen Applikation z.B. durch einen Parameter oder durch eine Konfigurationsdatei erfolgen.

Zum Schluss noch die Implementierung der einzelnen Formatter.

public class FormatterBase
{
   public virtual string Format(string message)
   {
       return message;
   }
}

public class FormatterTimeStamp : FormatterBase
{
   public override string Format(string message)
   {
       return string.Format("{0} - {1}",
                            DateTime.Now.ToShortTimeString(),
                            message);
   }
}

public class FormatterDateTimeStamp : FormatterBase
{
    public override string Format(string message)
    {
       return string.Format("{0} {1} - {2}",
                            DateTime.Now.ToShortDateString(),
                            DateTime.Now.ToShortTimeString(),
                            message);
    }
}

Wird das Programm gestartet, so erhält man folgende Ausgabe:

CommandWindowSample01

Beispiel 1 (Visual Studio 2010) auf GitHub

Beispiel 2

Das zweite Beispiel ist dem ersten sehr ähnlich. Der entscheidende Unterschied ist der Aufruf der Methode Log(). Der Typ des Formatters wird in diesem Fall mit angegeben.

using Formatter;
using Logger;

namespace Host
{
    class Program
    {
        [Import]
        public ConsoleLogger Logger { get; set; }

        static void Main(string[] args)
        {
            new Program().Run();
        }

        void Run()
        {
            var catalog = new DirectoryCatalog(".");
            var container = new CompositionContainer(catalog);
            container.ComposeParts(this);
            Logger.Log("Message", typeof(FormatterTimeStamp));
            Logger.Log("Message", typeof(FormatterDateTimeStamp));
        }
    }
}

Die Klasse ConsoleLogger enthält wiederum die Methode Log() und die Eigenschaft FormatterFunc. FormatterFunc ist aber diesmal ein Delegate vom Typ Func<Type, FormatterBase>. FormatterFunc erwartet einen Parameter vom Typ Type und liefert ein Objekt vom Typ FormatterBase zurück.

Auch die Methode Log() enthält zusätzlich einen Parameter vom Typ Type. In der Methode Log() wird über den Delegate FormatterFunc der gewünschte Formatter angefordert. Welche Methode über den Delegate genau aufgerufen wird, hängt davon ab, welche Methode das Managed Extensibility Framework an den Delegate gebunden hat. In diesem Beispiel ist es die Methode GetTextFormatter() aus der Klasse FormatterFactory.

using System;
using System.ComponentModel.Composition;
using Formatter;

namespace Logger
{
    [Export]
    public class ConsoleLogger
    {
        private FormatterBase formatterBase;

        [Import]
        private Func<Type, FormatterBase> FormatterFunc { get; set; }

        public void Log(string message, Type formatterType)
        {
            FormatterBase formatterBase = FormatterFunc(formatterType);
            string formattedString = formatterBase.Format(message);
            Console.WriteLine(formattedString);
        }
    }
}

Die Klasse FormatterFactory exportiert die passende Methode, die zu dem Delegate FormatterFunc der Klasse ConsoleLogger kompatibel ist. Innerhalb GetTextFormatter() wird von dem gewünschten Typ ein Objekt erzeugt und an die Methode Log() zurückgegeben. Hat die Methode Log() ein Formatter-Objekt erhalten, wird von diesem die Methode Format() aufgerufen und der formatierte Text ausgegeben.

public class FormatterFactory
{
   [Export]
    public FormatterBase GetTextFormatter(Type controlType)
    {
        ConstructorInfo ci = controlType.GetConstructor(new Type[] { });
        FormatterBase formatterBase = (FormatterBase)ci.Invoke(new object[] { });
        return formatterBase;
    }
}

Die Ausgabe des Beispiels:

CommandWindowSample02

Beispiel 2 (Visual Studio 2010) auf GitHub

Die beiden Beispiele zeigen recht gut, wie eine Anwendung aus eigenständigen Modulen bestehen kann, ohne dass der Programmieraufwand extrem ansteigt. Der Einsatz einer Class Factory hilft, die Flexibilität zu erhöhen.

Author: Stefan Henneken

I’m Stefan Henneken, a software developer based in Germany. This blog is just a collection of various articles I want to share, mostly related to Software Development.

One thought on “MEF Teil 7 – Exportieren über eine Class Factory”

Leave a comment