Muster in der Praxis

Interne domänenspezifische Sprachen

Jeremy Miller

Domänenspezifische Sprachen (Domain Specific Languages, DSLs) waren in den beiden letzten Jahren häufig im Gespräch und werden in den nächsten Jahren wahrscheinlich weiter an Bedeutung gewinnen. Möglicherweise verfolgen Sie bereits das Projekt "Oslo" (jetzt auch SQL Server-Modellierung genannt) oder experimentieren mit Tools wie ANTLR, um selbst "externe" DSLs zu erstellen. Eine greifbare Alternative besteht im Erstellen "interner" DSLs, die in einer gegebenen Programmiersprache wie C# geschrieben werden.

Interne DSLs sind für Nichtprogrammierer unter Umständen nicht ganz so einprägsam und lesbar wie externe DSLs, die wie Englisch klingen können. Die Erstellung interner DSLs ist jedoch einfacher, da Sie keine fremden Compiler oder Parser einsetzen müssen.

Beachten Sie bitte, dass ich nicht behaupte, die DSLs in diesem Artikel könnten von Experten aus dem betriebswirtschaftlichen Bereich beurteilt werden. In diesem Artikel konzentriere ich mich nur darauf, wie die Muster interner DSLs uns Entwicklern die Arbeit erleichtern können, indem wir APIs anfertigen, die leichter zu lesen und zu schreiben sind.

Ich zitiere eine Menge Beispiele aus zwei Open-Source-Projekten, die in C# geschrieben sind und die ich verwalte und entwickle. Das erste Projekt ist StructureMap, eines der IoC-Containertools (Inversion of Control) für Microsoft .NET Framework. Beim zweiten Projekt handelt es sich um StoryTeller, ein Tool für Akzeptanztests. Sie können den kompletten Quellcode der beiden Projekte über Subversion von https://structuremap.svn.sourceforge.net/svnroot/structuremap/trunk oder http://storyteller.tigris.org/svn/storyteller/trunk (Registrierung erforderlich) herunterladen. Als weitere Beispielquelle empfehle ich zudem das Projekt Fluent NHibernate (fluentnhibernate.org).

Literale Erweiterungen

Zu den wichtigeren Dingen, auf die ich in diesem Artikel hinweisen möchte, gehören die vielen kleinen Tricks, die Sie anwenden können, damit Ihr Code verständlicher und ausdrucksvoller wird. Diese kleinen Tricks können Ihre Arbeit als Programmierer wirklich aufwerten, indem sie es Ihnen erleichtern, fehlerfreien Code zu schreiben, da der Code deklarativer wird und eher Aufschluss über die ihm zugrunde liegende Intention gibt.

Immer häufiger verwende ich Erweiterungsmethoden für grundlegende Objekte wie Zeichenfolgen und Zahlen, um die Wiederholungshäufigkeit der Kern-APIs von .NET Framework zu reduzieren und die Lesbarkeit zu erhöhen. Dieses Muster zum Erweitern von Wertobjekten wird "literale Erweiterungen" genannt.

Beginnen wir mit einem simplen Beispiel. Mein aktuelles Projekt beinhaltet konfigurierbare Regeln für wiederkehrende und geplante Ereignisse. Anfänglich versuchten wir, eine kleine interne DSL zum Konfigurieren dieser Ereignisse zu erstellen (wir sind jetzt dabei, uns stattdessen der Verwendung externer DSL zuzuwenden). Diese Regeln sind stark von TimeSpan-Werten abhängig, die bestimmen, wie oft ein Ereignis auftreten soll, wann es beginnen soll und wann es endet. Dies sieht dann etwa wie der folgende Codeausschnitt aus:

x.Schedule(schedule =>
{
    // These two properties are TimeSpan objects
    schedule.RepeatEvery = new TimeSpan(2, 0, 0);
    schedule.ExpiresIn = new TimeSpan(100, 0, 0, 0);
});

Beachten Sie insbesondere "new TimeSpan(2, 0, 0)" und "new TimeSpan(100, 0, 0, 0)". Als erfahrener .NET Framework-Entwickler können Sie erkennen, dass diese beiden Codefragmente "2 Stunden" und "100 Tage" bedeuten. Sie mussten dazu aber etwas nachdenken, oder nicht?  Stattdessen soll die TimeSpan-Definition jetzt etwas lesbarer gestaltet werden:

x.Schedule(schedule =>
{
    // These two properties are TimeSpan objects
    schedule.RepeatEvery = 2.Hours();
    schedule.ExpiresIn = 100.Days();
});

Im obigen Beispiel habe ich einfach nur für das ganzzahlige Objekt Erweiterungsmethoden verwendet, die TimeSpan-Objekte zurückgeben:

public static class DateTimeExtensions
{
    public static TimeSpan Days(this int number)
    {
        return new TimeSpan(number, 0, 0, 0);
    }

    public static TimeSpan Seconds(this int number)
    {
        return new TimeSpan(0, 0, number);
    }
}

Was die Implementierung betrifft, stellt die Umstellung von "new TimeSpan(2, 0, 0, 0)" auf "2.Days()" keine große Änderung dar, doch welcher Ausdruck ist verständlicher?  Ich weiß, dass ich bei der Übersetzung von Geschäftsregeln in Code besser zwei Tage statt "eine Zeitspanne, die aus zwei Tagen, null Stunden und null Minuten besteht" sage. Die verständlichere Version des Codes lässt sich einfacher auf Richtigkeit überprüfen, und das ist für mich Grund genug, die Version mit dem Literalausdruck zu verwenden.

Semantisches Modell

Wenn ich eine neue DSL erstelle, muss ich zwei Probleme lösen. Erstens stellen mein Team und ich uns anfangs die Frage, wie wir die DSL auf eine logische und selbstbeschreibende Weise ausdrücken, sodass sie einfach zu verwenden ist. Ich versuche, bei der Beantwortung dieser Frage so weit wie möglich außer Acht zu lassen, wie die eigentliche Funktionalität strukturiert oder erstellt wird.

Beispielsweise ermöglicht das IoC-Containertool (Inversion of Control) von StructureMap es den Benutzern, den Container explizit in der "Registry DSL" von StructureMap zu konfigurieren:

var container = new Container(x =>
{
    x.For<ISendEmailService>().HttpContextScoped()
        .Use<SendEmailService>();
});

Wenn Sie mit der Syntax eines IoC-Containers noch nicht vertraut sind: Der Code besagt lediglich, dass eine Instanz des konkreten Typs SendEmailService zurückgegeben wird, wenn zur Laufzeit vom Container ein Objekt des Typs ISendEmailService angefordert wird. Durch den Aufruf von HttpContextScoped wird StructureMap angewiesen, die ISendEmailService-Objekte einem einzigen HttpRequest-Objekt "zuzuordnen". Dies bedeutet Folgendes: Wenn der Code in ASP.NET ausgeführt wird, ist für jede HTTP-Anforderung nur eine eindeutige ISendEmailService-Instanz vorhanden, gleichgültig, wie viele Male in einer HTTP-Anforderung ein ISendEmailService-Objekt angefordert wird.

Sobald ich eine Idee habe, wie die gewünschte Syntax aussehen könnte, muss ich noch die zentrale Frage beantworten, wie ich die DSL-Syntax im Detail mit dem Code verbinde, der das tatsächliche Verhalten implementiert. Der Code für das Verhalten könnte direkt in den DSL-Code eingefügt werden, sodass die Laufzeitaktionen unmittelbar in Expression Builder-Objekten ausgeführt werden. Außer bei äußerst simplen Fällen, rate ich jedoch dringend von diesem Vorgehen ab. Komponententests können bei Expression Builder-Klassen etwas schwierig sein, und das schrittweise Debuggen einer Fluent-Schnittstelle ist weder der Produktivität noch der geistigen Gesundheit zuträglich. Sie sollten sich eigentlich in die Lage versetzen, die Verhaltenselemente der DSL zur Laufzeit Komponententests unterziehen (vorzugsweise), debuggen und mit Problembehandlungsmethoden bearbeiten zu können, ohne die für eine Fluent-Schnittstelle typischen Codeumwege schrittweise im Debugger ausführen zu müssen.

Ich muss das Laufzeitverhalten erstellen, und ich muss eine DSL definieren, die die Absichten der DSL-Benutzer möglichst deutlich zum Ausdruck bringt. Meiner Erfahrung nach ist es äußerst hilfreich, das Laufzeitverhalten in ein "semantisches Modell" auszulagern, das von Martin Fowler als "Domänenmodell, das durch eine DSL gefüllt wird" definiert wurde (martinfowler.com/dslwip/SemanticModel.html).

Bemerkenswert ist am obigen Codeausschnitt vor allem, dass er keine echte Arbeit leistet. Mit diesen paar Zeilen DSL-Code wird lediglich das semantische Modell für den IoC-Container konfiguriert. Sie könnten die oben genannte Fluent-Schnittstelle umgehen und die Objekte für das semantische Modell wie folgt selbst erstellen:

var graph = new PluginGraph();
PluginFamily family = graph.FindFamily(typeof(ISendEmailService));

family.SetScopeTo(new HttpContextLifecycle());
Instance defaultInstance = new SmartInstance<SendEmailService>();
family.AddInstance(defaultInstance);
family.DefaultInstanceKey = defaultInstance.Name;

var container = new Container(graph);

Der Registry DSL-Code und der Code direkt darüber weisen ein identisches Laufzeitverhalten auf. Der DSL-Code erstellt lediglich ein Objektdiagramm aus den PluginGraph-, PluginFamily-, Instance- und HttpContextLifecycle-Objekten. Daher stellt sich die Frage: Warum ist es wichtig, sich mit zwei getrennten Modellen zu beschäftigen? 

Erstens bevorzuge ich als Benutzer definitiv die DSL-Version der beiden oben dargestellten Codebeispiele. Hier muss weit weniger Code geschrieben werden, sie bringt meine Absichten klarer zum Ausdruck und der Benutzer muss nicht viel über die Details von StructureMap wissen. Als Implementierer von StructureMap brauche ich eine einfache Möglichkeit, die Funktionalität in kleinen Komponenten zu erstellen und zu testen, und das ist bei einer reinen Fluent-Schnittstelle recht schwierig.

Dank des semantischen Modells konnte ich die Verhaltensklassen recht einfach erstellen und testen. Der DSL-Code an sich wird sehr einfach, weil er nur das semantische Modell konfiguriert.

Diese Trennung von DSL-Ausdruck und semantischem Modell hat sich im Lauf der Zeit als sehr vorteilhaft erwiesen. Aufgrund von Rückmeldungen der Nutzer werden Sie die DSL-Syntax häufig variieren müssen, um besser lesbare und schreibbare Ausdrücke zu erhalten. Diese Variation geht reibungsloser vonstatten, wenn Sie sich nicht so viele Gedanken darüber machen müssen, ob die Laufzeitfunktionalität durch diese Syntaxänderungen beeinträchtigt wird.

Andererseits war ich aufgrund der Tatsache, dass die DSL die offizielle API für StructureMap darstellt, verschiedentlich in der Lage, das interne semantische Modell zu erweitern und umzustrukturieren, ohne die DSL-Syntax verändern zu müssen. Dies ist nur ein weiteres Beispiel für die Vorzüge des "Separation of Concerns"-Prinzips des Softwareentwurfs.

Fluent-Schnittstellen und Ausdrucks-Generatoren

Eine Fluent-Schnittstelle ist ein API-Format, in dem durch Methodenverkettung eine kurze, lesbare Syntax erzeugt wird. Ich glaube, das bekannteste Beispiel hierfür ist die zunehmend beliebtere jQuery-Bibliothek für die JavaScript-Entwicklung. jQuery-Benutzer werden den folgenden Code schnell erkennen:

var link = $(‘<a></a>’).attr("href", "#").appendTo(binDomElement);
$(‘<span></span>’).html(binName).appendTo(link);

Eine Fluent-Schnittstelle ermöglicht es mir, den Code in ein kleineres Textfenster zu "verdichten", wodurch er potenziell verständlicher wird. Überdies hilft sie mir auch dabei, den Benutzern meiner API Hinweise zur Auswahl der richtigen Optionen zu geben. Der einfachste und vielleicht gängigste Trick bei der Erstellung einer Fluent-Schnittstelle besteht darin, ein Objekt sich selbst von aufgerufenen Methoden zurückgeben zu lassen (jQuery funktioniert im Wesentlichen so).

Ich verwende in StoryTeller eine einfache Klasse namens "HtmlTag" zum Erzeugen von HTML. Ich kann wie folgt schnell ein HtmlTag-Objekt mit Methodenverkettung erstellen:

var tag = new HtmlTag("div").Text("my text").AddClass("collapsible");

Intern gibt das HtmlTag-Objekt einfach sich selbst zurück, wenn Text und AddClass aufgerufen werden:

public HtmlTag AddClass(string className)
{
    if (!_cssClasses.Contains(className))
    {
        _cssClasses.Add(className);
    }

    return this;
}
public HtmlTag Text(string text)
{
    _innerText = text;
    return this;
}

In einem etwas komplizierteren Szenario können Sie die Fluent-Schnittstelle in zwei Teile aufspalten: das semantische Modell, das das Laufzeitverhalten bereitstellt (später mehr zu diesem Muster) und eine Folge von "Expression Builder"-Klassen, die die DSL-Grammatiken implementieren.

Ich verwende ein Beispiel dieses Musters in der StoryTeller-Benutzeroberfläche zur Definition von Tastaturkombinationen und dynamischen Menüs. Ich suchte nach einer schnellen, programmiertechnischen Möglichkeit, eine Tastenkombination für eine Aktion in der Benutzeroberfläche zu definieren. Weil sich die meisten von uns nicht alle Tastenkombinationen für jede Anwendung, mit der wir arbeiten, merken können, wollte ich außerdem ein Menü in der Benutzeroberfläche erstellen, das alle verfügbaren Verknüpfungen und die Tastenkombinationen, mit denen diese ausgeführt werden, bietet. Wenn Fenster im Hauptbereich der StoryTeller-Benutzeroberfläche aktiviert werden, wollte ich der Benutzeroberfläche auch ein Menü mit dynamischen Schaltflächen hinzufügen, die sich auf das aktivierte Fenster beziehen.

Ich hätte dies sicherlich nach der sprichwörtlichen WPF-Methode (Windows Presentation Foundation) programmieren können. Dies hätte aber bedeutet, dass ich verschiedene Bereiche des XAML-Markups für Tastaturgesten, Befehle, die Objekte der Menüleiste für jedes Fenster und die Menübefehle hätte bearbeiten und dann sicherstellen müssen, dass diese Elemente richtig zusammengefügt worden waren. Stattdessen wollte ich die Registrierung neuer Tastenkombinationen und Menübefehle so deklarativ wie möglich machen, und ich wollte den Oberflächenbereich des Codes auf einen Punkt reduzieren. Natürlich erstellte ich eine Fluent-Schnittstelle, die hinter den Kulissen alle disparaten WPF-Objekte für mich konfigurierte.

In der Praxis kann ich mit dem folgenden Code eine globale Tastenkombination festlegen, um das Fenster "Execution Queue" zu öffnen:

// Open the "Execution Queue" screen with the 
// CTRL - Q shortcut
Action("Open the Test Queue")
    .Bind(ModifierKeys.Control, Key.Q)
    .ToScreen<QueuePresenter>();

Im Bildschirmaktivierungscode für ein Fenster kann ich mit Code wie dem Folgenden temporäre Tastenkombinationen und die dynamischen Menüoptionen in der Hauptanwendungsshell definieren:

screenObjects.Action("Run").Bind(ModifierKeys.Control, Key.D1)
      .To(_presenter.RunCommand).Icon = Icon.Run;

   screenObjects.Action("Cancel").Bind(ModifierKeys.Control, Key.D2)
      .To(_presenter.CancelCommand).Icon = Icon.Stop;

   screenObjects.Action("Save").Bind(ModifierKeys.Control, Key.S)
      .To(_presenter.SaveCommand).Icon = Icon.Save;

Sehen wir uns jetzt die Implementierung dieser Fluent-Schnittstelle an. Ihr liegt eine semantische Modellklasse namens ScreenAction zugrunde, die die eigentliche Arbeit leistet und die einzelnen WPF-Objekte erstellt. Diese Klasse sieht folgendermaßen aus:

public interface IScreenAction
{
    bool IsPermanent { get; set; }
    InputBinding Binding { get; set; }
    string Name { get; set; }
    Icon Icon { get; set; }
    ICommand Command { get; }
    bool ShortcutOnly { get; set; }
    void BuildButton(ICommandBar bar);
}

Dies ist ein wichtiges Detail. Ich kann das ScreenAction-Objekt unabhängig von der Fluent-Schnittstelle erstellen und testen, und die Fluent-Schnittstelle muss jetzt bloß ScreenAction-Objekte konfigurieren. Die eigentliche DSL wird in einer Klasse namens ScreenObjectRegistry implementiert, die die Liste der aktiven ScreenAction-Objekte nachverfolgt (siehe Abbildung 1).

Abbildung 1 DSL-Implementierung für ScreenActionClass

public class ScreenObjectRegistry : IScreenObjectRegistry
 {
     private readonly List<ScreenAction> _actions = 
        new List<ScreenAction>();
     private readonly IContainer _container;
     private readonly ArrayList _explorerObjects = new ArrayList();
     private readonly IApplicationShell _shell;
     private readonly Window _window;

     public IEnumerable<ScreenAction> Actions { 
        get { return _actions; } }


     public IActionExpression Action(string name)
     {
         return new BindingExpression(name, this);
     }

     // Lots of other methods that are not shown here
 }

Die Registrierung einer neuen Bildschirmaktion bzw. eines ScreenAction-Objekts beginnt mit einem Aufruf der oben gezeigten Action(name)-Methode und gibt eine neue Instanz der BindingExpression-Klasse zurück, die als Ausdrucks-Generator fungiert, um das neue ScreenAction-Objekt zu konfigurieren. Dieser Vorgang wird teilweise in Abbildung 2 dargestellt.

Abbildung 2 Die BindingExpression-Klasse fungiert als Ausdrucks-Generator

public class BindingExpression : IBindingExpression, IActionExpression
{
    private readonly ScreenObjectRegistry _registry;
    private readonly ScreenAction _screenAction = new ScreenAction();
    private KeyGesture _gesture;

    public BindingExpression(string name, ScreenObjectRegistry registry)
    {
        _screenAction.Name = name;
        _registry = registry;
    }

    public IBindingExpression Bind(Key key)
    {
        _gesture = new KeyGesture(key);
        return this;
    }

    public IBindingExpression Bind(ModifierKeys modifiers, Key key)
    {
        _gesture = new KeyGesture(key, modifiers);
        return this;
    }

    // registers an ICommand that will launch the dialog T
    public ScreenAction ToDialog<T>()
    {
        return buildAction(() => _registry.CommandForDialog<T>());
    }

    // registers an ICommand that would open the screen T in the 
    // main tab area of the UI
    public ScreenAction ToScreen<T>() where T : IScreen
    {
        return buildAction(() => _registry.CommandForScreen<T>());
    }

    public ScreenAction To(ICommand command)
    {
        return buildAction(() => command);
    }

    // Merely configures the underlying ScreenAction
    private ScreenAction buildAction(Func<ICommand> value)
    {
        ICommand command = value();
        _screenAction.Binding = new KeyBinding(command, _gesture);

        _registry.register(_screenAction);

        return _screenAction;
    }

    public BindingExpression Icon(Icon icon)
    {
        _screenAction.Icon = icon;
        return this;
    }
}

Einer der wichtigsten Faktoren bei vielen Fluent-Schnittstellen ist der Versuch, den Benutzer der API dazu anzuleiten, bestimmte Dinge in einer bestimmten Reihenfolge zu tun. Im Fall von Abbildung 2 verwende ich die Schnittstellen von BindingExpression nur, um die Benutzeroptionen in IntelliSense zu steuern, obwohl die ganze Zeit über dasselbe BindingExpression-Objekt zurückgegeben wird. Denken Sie einmal darüber nach. Die Benutzer dieser Fluent-Schnittstelle sollen den Aktionsnamen und die Tasten für die Tastenkombination nur einmal angeben. Danach sollte der Benutzer diese Methoden in IntelliSense nicht mehr zu Gesicht bekommen. Der DSL-Ausdruck beginnt mit dem Aufruf von ScreenObjectRegistry.Action(name), womit der beschreibende Name der Tastenkombination erfasst wird, der in Menüs angezeigt wird. Zurückgegeben wird ein neues BindingExpression-Objekt als Schnittstelle:

public interface IActionExpression   
{
    IBindingExpression Bind(Key key);
    IBindingExpression Bind(ModifierKeys modifiers, Key key);
}

Durch die Typumwandlung der BindingExpression-Instanz in eine IActionExpression-Instanz, hat der Benutzer lediglich die Möglichkeit, die Tastenkombination für die Verknüpfung anzugeben. Daraufhin wird dasselbe BindingExpression-Objekt zurückgegeben, das jedoch in eine IBindingExpression-Schnittstelle umgewandelt wurde, die nur zulässt, dass die Benutzer eine einzelne Aktion angeben:

// The last step that captures the actual
// "action" of the ScreenAction
public interface IBindingExpression
{
    ScreenAction ToDialog<T>();
    ScreenAction ToScreen<T>() where T : IScreen;
    ScreenAction PublishEvent<T>() where T : new();
    ScreenAction To(Action action);
    ScreenAction To(ICommand command);
}

Objektinitialisierer

Nachdem wir die Methodenverkettung als Hauptstütze der Entwicklung interner DSLs in C# vorgestellt haben, wollen wir uns alternativen Mustern zuwenden, die für DSL-Entwickler häufig zu einfacheren Mechanismen führen können. Die erste Alternative besteht einfach in der Verwendung der Funktionalität der Objektinitialisierer, die in Microsoft .NET Framework 3.5 eingeführt wurde.

Ich kann mich noch an meinen allerersten Vorstoß in Fluent-Schnittstellen erinnern. Ich arbeitete an einem System, das als Nachrichtenmakler zwischen Rechtsanwaltskanzleien, die Rechnungen auf elektronischem Wege übermittelten, und deren Kunden fungierte. Einer der üblichen Anwendungsfälle bestand für uns darin, im Namen der Rechtsanwaltskanzleien Nachrichten an deren Kunden zu senden. Zum Senden der Nachrichten riefen wir eine Schnittstelle wie folgt auf:

public interface IMessageSender
{
    void SendMessage(string text, string sender, string receiver);
}

Das ist eine sehr einfache API; es wird einfach ein Argument mit drei Zeichenfolgen übergeben, und dann ist sie startklar. In der Praxis stellt sich das Problem, welches Argument wohin gehört. Ja, Tools wie ReSharper können einem jederzeit zeigen, welchen Parameter Sie zu einem gegebenen Zeitpunkt angeben. Wie steht es aber mit der Überprüfung der SendMessage-Aufrufe, wenn nur der Code gelesen wird? Sehen Sie sich die Syntax des folgenden Codebeispiels an, und Sie wissen genau, was ich meine, wenn ich von aus der Vertauschung der Zeichenfolgenargumente herrührenden Fehlern spreche:

// Snippet from a class that uses IMessageSender
 public void SendMessage(IMessageSender sender)
 {
     // Is this right?
     sender.SendMessage("the message body", "PARTNER001", "PARTNER002");

     // or this?
     sender.SendMessage("PARTNER001", "the message body", "PARTNER002");

     // or this?
     sender.SendMessage("PARTNER001", "PARTNER002", "the message body");
 }

Damals löste ich das Problem mit der Benutzerfreundlichkeit der API, indem ich mir einen Fluent-Schnittstellenansatz zu eigen machte, aus dem deutlicher hervorging, welches Argument für was stand:

public void SendMessageFluently(FluentMessageSender sender)
 {
     sender
         .SendText("the message body")
         .From("PARTNER001").To("PARTNER002");
 }

Ich war wirklich davon überzeugt, dass sich daraus eine brauchbarere, weniger fehleranfällige API ergab. Aber sehen wir uns in Abbildung 3 an, wie die zugrunde liegende Implementierung der Ausdrucks-Generatoren aussehen könnte.

Abbildung 3 Implementieren eines Ausdrucks-Generators

public class FluentMessageSender
{
    private readonly IMessageSender _messageSender;

    public FluentMessageSender(IMessageSender sender)
    {
        _messageSender = sender;
    }

    public SendExpression SendText(string text)
    {
        return new SendExpression(text, _messageSender);
    }

    public class SendExpression : ToExpression
    {
        private readonly string _text;
        private readonly IMessageSender _messageSender;
        private string _sender;

        public SendExpression(string text, IMessageSender messageSender)
        {
            _text = text;
            _messageSender = messageSender;
        }

        public ToExpression From(string sender)
        {
            _sender = sender;
            return this;
        }

        void ToExpression.To(string receiver)
        {
            _messageSender.SendMessage(_text, _sender, receiver);
        }
    }

    public interface ToExpression
    {
        void To(string receiver);
    }
}

Hier ist viel mehr Code zum Erstellen der API erforderlich als bei der ursprünglichen Version. Glücklicherweise verfügen wir jetzt über eine weitere Alternative mit Objektinitialisierern (oder mit benannten Parametern in .NET Framework 4 oder VB.NET). Jetzt erstellen wir eine weitere Version des Nachrichtensenders, die nur ein Objekt als Parameter akzeptiert:

public class SendMessageRequest
{
    public string Text { get; set; }
    public string Sender { get; set; }
    public string Receiver { get; set; }
}

public class ParameterObjectMessageSender
{
    public void Send(SendMessageRequest request)
    {
        // send the message
    }
}

Die API-Syntax mit einem Objektinitialisierer lautet wie folgt:

public void SendMessageAsParameter(ParameterObjectMessageSender sender)
 {
     sender.Send(new SendMessageRequest()
     {
         Text = "the message body",
         Receiver = "PARTNER001",
         Sender = "PARTNER002"
     });
 }

Diese dritte Inkarnation der API verringert Bedienungsfehler wohl mit einem viel einfacheren Mechanismus als die Fluent-Schnittstellen-Variante.

Der springende Punkt ist hier, dass Fluent-Schnittstellen nicht das einzige Muster zum Erstellen verständlicherer APIs in .NET Framework sind. Dieser Ansatz ist in JavaScript viel üblicher, wo man die JavaScript Object Notation (JSON) verwenden kann, um Objekte in einer Codezeile vollständig zu definieren, und in Ruby, wo typischerweise Namen-Wert-Hashes als Methodenargumente verwendet werden.

Nested Closure

Ich denke, viele nehmen an, dass Fluent-Schnittstellen und Methodenverkettung die einzigen Möglichkeiten zum Erstellen von DSLs in C# darstellen. Auch ich war dieser Meinung. Seitdem habe ich jedoch andere Techniken und Muster gefunden, die häufig viel einfacher zu implementieren sind als die Methodenverkettung. Ein Muster, das zunehmend beliebter wird, ist das "Nested-Closure"-Muster:

Drücken Sie Anweisungselemente eines Funktionsaufrufs aus, indem Sie sie in einem Argument in einen Closure einfügen.

In immer mehr .NET Web-Entwicklungsprojekten wird das Model-View-Controller-Muster verwendet. Ein Nebeneffekt dieser Bewegung ist, dass in Eingabeelementen viel mehr HTML generiert werden muss. Die direkte Zeichenfolgenbearbeitung zur Erzeugung von HTML kann schnell unangenehm werden. Am Schluss müssen viele Aufrufe wiederholt werden, um den HTML-Code "keimfrei zu machen" oder um Injektionsangriffe zu verhindern und in vielen Fällen sollen mehrere Klassen oder Methoden an der endgültigen HTML-Darstellung beteiligt sein können. Ich möchte die Erstellung von HTML einfach durch die Aussage "ich möchte ein div-Tag mit diesem Text und dieser Klasse" ausdrücken können. Um diese Art der HTML-Generierung zu erleichtern, modellieren wir HTML mit einem "HtmlTag"-Tagobjekt, das etwa wie folgt aussieht

var tag = new HtmlTag("div").Text("my text").AddClass("collapsible");
Debug.WriteLine(tag.ToString());

und den folgenden HTML-Code generiert:

<div class="collapsible">my text</div>

Den Kern dieses HTML-Generierungsmodells bildet das HtmlTag-Objekt, das über Methoden zum programmgesteuerten Aufbau einer HTML-Elementstruktur verfügt und folgendermaßen aussieht:

public interface IHtmlTag
{
    HtmlTag Attr(string key, object value);
    HtmlTag Add(string tag);
    HtmlTag AddStyle(string style);
    HtmlTag Text(string text);
    HtmlTag SetStyle(string className);
    HtmlTag Add(string tag, Action<HtmlTag> action);
}

Dieses Modell lässt auch das Hinzufügen verschachtelter HTML-Tags zu:

[Test]
public void render_multiple_levels_of_nesting()
{
    var tag = new HtmlTag("table");
    tag.Add("tbody/tr/td").Text("some text");

    tag.ToCompacted().ShouldEqual(
       "<table><tbody><tr><td>some text</td></tr></tbody></table>"
    );
}

In der Praxis stelle ich oft fest, dass ich gern in einem Schritt ein voll konfiguriertes untergeordnetes Tag hinzufügen würde. Wie bereits erwähnt, habe ich ein Open-Source-Projekt namens StoryTeller, das mein Team zur Formulierung von Funktionstests nutzt. Ein Teil der Funktionalität von StoryTeller besteht in der Ausführung sämtlicher Funktionstests in unserem fortlaufenden Integrationsbuild und der Erstellung eines Berichts über die Testergebnisse. Die Zusammenfassung der Testergebnisse wird als einfache Tabelle mit drei Spalten dargestellt. Der HTML-Code für die Zusammenfassungstabelle sieht folgendermaßen aus:

<table>
    <thead>
        <tr>
            <th>Test</th>
            <th>Lifecycle</th>
            <th>Result</th>
        </tr>
    </thead>
    <tbody>
        <!-- rows for each individual test -->
    </tbody>
</table>

Unter Verwendung des oben beschriebenen HtmlTag-Modells generiere ich mit folgendem Code die Spaltenkopfstruktur der Ergebnistabelle:

// _table is an HtmlTag object

// The Add() method accepts a nested closure argument
 _table.Add("thead/tr", x =>
{
    x.Add("th").Text("Test");
    x.Add("th").Text("Lifecycle");
    x.Add("th").Text("Result");
});

Im Aufruf von _table.Add übergebe ich eine Lambda-Funktion, die vollständig definiert, wie die erste Zeile mit den Spaltenköpfen erzeugt werden soll. Der Einsatz des Nested-Closure-Musters ermöglicht es mir, die Spezifikation zu übergeben, ohne zuerst eine weitere Variable für das "tr"-Tag definieren zu müssen. Möglicherweise gefällt Ihnen diese Syntax nicht auf den ersten Blick, der Code wird damit jedoch kürzer. Intern sieht die Add-Methode, in der das Nested-Closure-Muster verwendet wird, einfach so aus:

public HtmlTag Add(string tag, Action<HtmlTag> action)
{
    // Creates and adds the new HtmlTag with
    // the supplied tagName
    var element = Add(tag);

    // Uses the nested closure passed into this
    // method to configure the new child HtmlTag
    action(element);

    // returns that child
    return element;
}

Ein weiteres Beispiel ist die zentrale StructureMap-Klasse namens Container, zu deren Initialisierung ein Nested Closure übergeben wird, der alle gewünschten Containerkonfigurationen wie folgt darstellt:

IContainer container = new Container(r =>
{
    r.For<Processor>().Use<Processor>()
        .WithCtorArg("name").EqualTo("Jeremy")
        .TheArrayOf<IHandler>().Contains(x =>
        {
            x.OfConcreteType<Handler1>();
            x.OfConcreteType<Handler2>();
            x.OfConcreteType<Handler3>();
        });
});

Die Signatur und der Textkörper für diese Konstruktorfunktion lauten wie folgt:

public Container(Action<ConfigurationExpression> action)
{
    var expression = new ConfigurationExpression();
    action(expression);

    // As explained later in the article,
    // PluginGraph is part of the Semantic Model
    // of StructureMap
    PluginGraph graph = expression.BuildGraph();

    // Take the PluginGraph object graph and
    // dynamically emit classes to build the
    // configured objects
    construct(graph);
}

Ich verwendete das Nested-Closure-Muster in diesem Fall aus verschiedenen Gründen. Der erste Grund bestand in der Tatsache, dass der StructureMap-Container zuerst die komplette Konfiguration in einem Schritt liest und dann mithilfe von Reflection.Emit dynamisch "Generator"-Objekte erzeugt, bevor der Container verwendet werden kann. Die Übergabe der Konfiguration in einem verschachtelten Closure ermöglicht es mir, die gesamte Konfiguration gleichzeitig zu erfassen und die Ausgabe stillschweigend vorzunehmen, unmittelbar bevor der Container zur Nutzung zur Verfügung gestellt wird. Der andere Grund besteht in der Trennung der Methoden, mit denen die Typen zum Zeitpunkt der Konfiguration im Container registriert werden, von den Methoden, die zur Laufzeit zum Abruf von Diensten verwendet werden (dies ist ein Beispiel für das Schnittstellentrennungsprinzip – engl. Interface Segregation Principle – das "I" in S.O.L.I.D).

Ich habe das Nested-Closure-Muster in diesen Artikel aufgenommen, weil es in Open-Source-Projekten für .NET Framework, wie beispielsweise Rhino Mocks, Fluent NHibernate und vielen IoC-Tools, zum vorherrschenden Muster wird. Außerdem habe ich festgestellt, dass Nested-Closure-Muster häufig bedeutend einfacher zu implementieren sind als die ausschließliche Verwendung der Methodenverkettung. Der Nachteil ist, dass viele Entwickler wenig Erfahrung im Umgang mit Lambda-Ausdrücken haben. Überdies ist diese Technik in VB.NET kaum brauchbar, weil VB.NET mehrzeilige Lambda-Ausdrücke nicht unterstützt.

IronRuby und Boo

Alle Beispiele in diesem Artikel sind in C# geschrieben, damit sie für möglichst viele Leser ansprechend sind, wenn die DSL-Entwicklung Sie jedoch interessiert, sollten Sie sich andere CLR-Sprachen ansehen. Wegen der flexiblen und verhältnismäßig schnörkellosen Syntax (optionale Klammern, keine Semikolons und sehr knapp) eignet sich insbesondere IronRuby außergewöhnlich gut zum Erstellen interner DSLs. Wenn man sich etwas weiter draußen umsieht, wird auch die Sprache Boo gerne zur DSL-Entwicklung in der CLR verwendet.

Die Namen und Definitionen der Entwurfsmuster wurden dem Onlineentwurf des in Kürze erscheinenden Buchs von Martin Fowler zum Thema Domain Specific Languages entnommen, der verfügbar ist unter http://martinfowler.com/dsl.html.

Jeremy Miller ist ein Microsoft-MVP für C# und zudem der Entwickler des Open-Source-Tools StructureMap (structuremap.sourceforge.net) für Abhängigkeitsinjektion mit .NET und des in Kürze veröffentlichen Tools StoryTeller (http://storyteller.tigris.org/svn/storyteller/trunk) für FitNesse-Tests in .NET. Besuchen Sie seinen Blog "The Shade Tree Developer" unter codebetter.com/blogs/jeremy.miller, der Teil der CodeBetter-Website ist.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Glenn Block