Peter Bucher Ralf Westphal

Blog

Nutze den Augenblick
und teile der Welt mit, was Du zu sagen hast.

Microkernel im Eigenbau

Freitag, 13. März 2009, 21:17 Uhr
Permalink | Kommentare (9) | Kommentare als RSSRSS

Ende November des vergangenen Jahres habe ich in Architektur lernen empfohlen, sich mit dem Einsatz eines Microkernels zu beschäftigen:

Auch dieses Thema fördert das Denken in Schnittstellen, allerdings auf einer anderen Ebene als die Entwurfsmuster - beziehen diese sich nämlich auf das Klassendesign, bezieht sich der Einsatz eines Microkernels auf das Komponentendesign einer Anwendung.

Außerdem habe ich behauptet, dass es allen bestehenden Microkerneln zum Trotz nicht schwierig sei, einen einfachen Microkernel selbst zu implementieren:

Zwar gibt es einige Projekte wie beispielsweise Unity, die man sich näher anschauen kann, allerdings ist ein einfacher Microkernel in wenigen Zeilen selbst implementiert, wobei der Lerneffekt dann allerdings weitaus größer ist.

Da ich seither immer wieder nach einem Beispiel gefragt werde, beschreibe ich nun hier, wie man einen eigenen Microkernel erstellt. Ich werde mich dabei auf das wesentliche Feature – nämlich die Instanziierung eines konkreten Typen an Hand eines gegebenen Contracts – beschränken, und Aspekte wie Fehlerbehandlung, Performance, Sicherheit und Konfigurierbarkeit außen vor lassen.

Letztlich sind folgende Schritte nötig, um einen eigenen lauffähigen Microkernel zu entwickeln:

  • Erzeugen einer Konfiguration, die das Mapping von Contracts auf konkrete Typen übernimmt. Es bietet sich an, diese Konfiguration in einer XML-Datei abzulegen, da diese erstens dank LINQ to XML mit wenig Aufwand ausgelesen werden kann, und dies zum anderen ermöglicht, die Konfiguration des Microkernels zu ändern, ohne das Projekt neu kompilieren zu müssen.
  • Instanziieren des für einen gegebenen Contract definierten konkreten Typs. Dies geschieht im Wesentlichen mit Hilfe der Activator-Klasse von .NET, wobei so wohl in das Projekt integrierte Assemblies wie auch zusätzliche Assemblies unterstützt werden sollen.

Zunächst muss also eine Konfiguration erzeugt werden, die das Mapping von einem Contract auf einen konkreten Typen übernimmt. Dazu dient folgende Struktur, die das Grundgerüst einer entsprechenden XML-Datei darstellt:

<?xml version="1.0" encoding="utf-8" ?>
<serviceLocator>
  <mappings>
    <mapping contract="" type="" />
  </mappings>
</serviceLocator>

Als einfaches Beispiel soll eine Instanz des Typs MemoryStream im Namensraum System.IO verwendet und auf die Schnittstelle IDisposable gemappt werden.

Hierzu müssen lediglich die Namen der beiden Typen in der XML-Datei angegeben werden, wobei diese vollqualifiziert  – also einschließlich des jeweiligen Namensraums – angegeben werden müssen:

<?xml version="1.0" encoding="utf-8" ?>
<serviceLocator>
  <mappings>
    <mapping contract="System.IDisposable" type="System.IO.MemoryStream" />
  </mappings>
</serviceLocator>

Im nächsten Schritt wird eine statische Klasse ServiceLocator erzeugt, die über die folgenden Methoden verfügt:

  • Einen statischen Konstruktur: Dieser ist dafür zuständig, die Konfiguration aus der XML-Datei auszulesen, und die dort angegebenen Namen der Contracts und Typen in jeweils eine Instanz der Type-Klasse zu überführen, und diese Zuordnungen danach in einem Dictionary abzuspeichern.
  • Eine statische Methode namens GetService: Diese Methode hat als einzige Aufgabe, den konkreten Typ für den angeforderten Contract aus dem Dictionary zu ermitteln, diesen zu instanziieren und entsprechend gecastet an den Aufrufer zurückzugeben.

Insgesamt ergibt sich daraus die folgende Grundstruktur für die Klasse ServiceLocator:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace CherryFlavored.NET.Core.Kernel
{
    /// <summary>
    /// Represents a service locator.
    /// </summary>
    public static class ServiceLocator
    {
        /// <summary>
        /// Contains the mappings from contracts to types.
        /// </summary>
        private static Dictionary<Type, Type> _mappings;

        /// <summary>
        /// Initializes the <see cref="ServiceLocator" /> type.
        /// </summary>
        static ServiceLocator()
        {
        }

        /// <summary>
        /// Gets an instance for the specified contract.
        /// </summary>
        /// <typeparam name="TContract">The type of the contract.</typeparam>
        /// <returns>An instance that matches the specified contract.</returns>
        public static TContract GetService<TContract>() where TContract : class
        {
        }
    }
}

Um die Konfigurationsdatei auszulesen und das Dictionary entsprechend zu erzeugen, bietet sich – wie oben bereits erwähnt – die Verwendung von LINQ to XML an:

// Load the configuration.
_mappings =
    (from mapping in
         XElement.Load("ServiceLocator.xml").Element("mappings").Elements("mapping")
     select
         new KeyValuePair<Type, Type>(
             Type.GetType(mapping.Attribute("contract").Value),
             Type.GetType(mapping.Attribute("type").Value))).ToDictionary(
        kvp => kvp.Key, kvp => kvp.Value);

Die Instanziierung des konkreten Typs in der Methode GetService ist sogar noch wesentlich einfacher, und kann in einer einzigen Zeile erfolgen:

// Instantiate the type and return it to the caller.
return (TContract)Activator.CreateInstance(_mappings[typeof(TContract)]);

Um nun tatsächlich eine Instanz der Klasse MemoryStream an Hand ihres Contracts zu erzeugen, genügt eine Referenz auf den Microkernel und folgende Zeile:

// Try to get an instance for an IDisposable type.
IDisposable memoryStream = ServiceLocator.GetService<IDisposable>();

Dieser Microkernel kann nun bereits verwendet werden, um Komponenten innerhalb eines Projektes zu entkoppeln. Häufig ist der zu instanziierende Typ aber gar nicht Bestandteil des eigentlichen Projektes, sondern liegt in einer eigenständigen Assembly, die potenziell erst nach dem Kompilieren zu einer bereits erfolgten Installation beigefügt wird.

Alles, was zur Kompilierungszeit in einem solchen Fall zur Verfügung steht, ist der Contract – der eigentliche Typ wird unabhängig davon bereitgestellt, um beispielsweise Addins zu ermöglichen.

Um ein solches Szenario zu unterstützen, muss der Microkernel also eine Möglichkeit bieten, einen Typ auch aus einer Assembly zu laden, die neben dem eigentlichen Projekt losgelöst im Dateisystem liegt, ohne dass sie bereits zur Kompilierzeit verfügbar war.

Das schöne ist: Dieses Szenario wird bereits unterstützt. Alles, was hierfür notwendig ist, ist, in der Konfigurationsdatei neben der Klasse auch noch die enthaltende Assembly anzugeben, wie folgendes Beispiel an Hand der Post-Klasse von BlogEngine.NET zeigt:

<?xml version="1.0" encoding="utf-8" ?>
<serviceLocator>
  <mappings>
    <mapping contract="System.IDisposable" type="BlogEngine.Core.Post, BlogEngine.Core" />
  </mappings>
</serviceLocator>

Mehr ist für einen eigenen Microkernel prinzipiell nicht notwendig. Natürlich empfiehlt es sich, diesen für einen produktiven Einsatz noch an etlichen Stellen zu optimieren und um weitere Funktionen zu ergänzen. Die grundlegende Funktionsweise dürfte aber klar geworden sein.

Kommentare

Kommentar schreiben


(Zeigt dein Gravatar icon)  

  Country flag

biuquote
  • Kommentar
  • Live Vorschau
Loading