Шаблоны на практике

Внутренние языки, специфичные для предметной области

Джереми Миллер

Языки, специфичные для предметной области (Domain Specific Languages, DSL), были популярной темой за последнюю пару лет и, вероятно, их значение в предстоящие годы станет еще больше. Возможно, вы уже следите за проектом "Oslo" (теперь он называется SQL Server Modeling) или экспериментируете с такими инструментами, как ANTLR, для создания "внешних" DSL-языков. Но более доступная альтернатива — создание "внутренних" DSL-языков, которые пишутся на одном из существующих языков программирования вроде C#.

Внутренние DSL могут быть не столь выразительными и понятными людям, не имеющим отношения к программированию, по сравнению с внешними DSL, которые можно читать просто как английский текст, но механика создания внутреннего DSL проще, потому что в этом случае вы не применяете компиляторы или синтаксические анализаторы, внешние для вашего кода.

Заметьте: я не предполагаю, что какой-либо DSL в этой статье годится для анализа экспертами из той или иной области бизнеса. Здесь я уделю основное внимание только тому, как шаблоны внутренних DSL могут облегчить нашу работу как разработчикам, создавая API, более простые в чтении и написании.

Большое количество примеров я заимствовал из двух проектов с открытым исходным кодом на C#, которыми руковожу и в разработке которых я принимаю участие. Первый из них — StructureMap, один из инструментов Inversion of Control (IoC) Container для Microsoft .NET Framework. Второй — StoryTeller, инструмент приемочного тестирования. Вы можете скачать полный исходный код обоих проектов через Subversion по ссылкам https://structuremap.svn.sourceforge.net/svnroot/structuremap/trunk и storyteller.tigris.org/svn/storyteller/trunk (нужна регистрация). Как еще один источник примеров могу предложить и проект Fluent NHibernate (fluentnhibernate.org).

Литеральные расширения

Один из важных моментов, на которые я хотел бы обратить ваше внимание в статье, заключается в том, что существует масса простых приемов, позволяющих сделать код более читаемым, ясным и выразительным. Эти приемы могут реально помочь вам писать корректный код, так как в этом случае он становится более декларативным и четче раскрывает ваши намерения.

Все чаще и чаще я использую методы расширения применительно к элементарным объектам вроде строк и чисел, чтобы уменьшить их повторяемость в базовых .NET Framework API и улучшить читаемость кода. Этот шаблон расширения значимых объектов (value objects) называется литеральными расширениями (literal extensions).

Начнем с упрощенного примера. Мой текущий проект включает настраиваемые правила повторно возникающих (reoccurring) и регулярных (scheduled) событий. Изначально мы пытались создать небольшой внутренний DSL для конфигурирования этих событий (сейчас мы находимся в процессе перехода на внешний DSL). Эти правила полагаются на значения TimeSpan, определяющие, насколько часто должно происходить некое событие, когда оно должно начинаться и когда должен истекать срок его действия. Вот небольшой фрагмент, иллюстрирующий сказанное:

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

В частности, обратите внимание на "new TimeSpan(2, 0, 0)" и "new TimeSpan(100, 0, 0, 0)". Как опытный разработчик для .NET Framework вы можете догадаться, что эти два кусочка кода подразумевают "2 часа" и "100 дней", но вам пришлось поразмыслить, не правда ли? Вместо этого сделаем определение TimeSpan более читаемым:

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

Все, что я сделал в примере выше, — применил некоторые методы расширения к целочисленному объекту, которые возвращают объекты TimeSpan:

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);
    }
}

В терминах реализации переключение с "new TimeSpan(2, 0, 0, 0)" на "2.Days()" не слишком значимое изменение, но какое из них легче читать? Но переводя бизнес-правила в код, я скорее сказал бы "два дня", а не "промежуток времени, состоящий из двух дней, нуля часов и нуля минут". Более читаемую версию кода легче проверять на корректность, и для меня одного этого достаточно, чтобы использовать версию с литеральным выражением.

Семантическая модель

Когда я создаю новый DSL, мне нужно решить две задачи. Во-первых, моя группа и я начинаем с того, что задаем сами себе вопросы насчет того, как выразить DSL логичным и понятным способом, чтобы им можно было легко пользоваться. По возможности я стараюсь делать это без учета того, как будет структурироваться или создаваться реальная функциональность.

Например, утилита StructureMap для работы с контейнером Inversion of Control (IoC) позволяет явным образом настраивать контейнер в рамках Registry DSL:

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

Если вы еще не знакомы с использованием контейнера IoC, то подскажу: этот код всего лишь объявляет, что при запросе в период выполнения из контейнера какого-либо объекта типа ISendEmailService вы получите экземпляр конкретного типа SendEmailService. Вызов HttpContextScoped сообщает StructureMap ограничить область видимости объектов ISendEmailService единственным HttpRequest, и, если код выполняется в ASP.NET, для каждого индивидуального HTTP-запроса будет один уникальный экземпляр ISendEmailService независимо от того, сколько раз вы запрашивали ISendEmailService в рамках одного HTTP-запроса.

После выбора нужного синтаксиса остается важнейший вопрос: как именно соединить DSL-синтаксис с кодом, реализующим реальное поведение. Можно было бы поместить этот код прямо в DSL-код, чтобы операции периода выполнения происходили непосредственно в объектах Expression Builder, но я настоятельно не рекомендую такой подход — разве что в простейших случаях. К классам Expression Builder весьма затруднительно применять модульное тестирование, а отладка с пошаговым проходом через переменный (fluent) интерфейс отнюдь не способствует не то что повышению производительности труда, а даже сохранению здравого ума. Безусловно, вы предпочтете возможность модульного тестирования, отладки и выявления проблем с поведением элементов вашего DSL в период выполнения без пошагового прохода всего абстрагируемого кода в типичном переменном интерфейсе.

Мне нужно создать поведение периода выполнения и придумать такой DSL, который предельно четко выражает намерения пользователя этого DSL. Как показывает мой опыт, крайне полезно выделять поведение периода выполнения в "семантическую модель", определенную Мартином Фаулером (Martin Fowler) как "модель предметной области, заполняемую DSL" (martinfowler.com/dslwip/SemanticModel.html).

Ключевой момент в предыдущем фрагменте кода заключается в том, что он не выполняет никакой реальной работы. Этот кусочек DSL-кода лишь настраивает семантическую модель контейнера IoC. Вы могли бы обойти переменный интерфейс, показанный выше, и самостоятельно создать объекты семантической модели, например:

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);

Код Registry DSL и предыдущий код идентичны по своему поведению в период выполнения. DSL лишь создает граф из объектов PluginGraph, PluginFamily, Instance и HttpContextLifecycle. Возникает вопрос, а зачем возиться с двумя раздельными моделями?

Прежде всего, как пользователю мне определенно нужна DSL-версия из двух предыдущих примеров кода, потому что она позволяет писать гораздо меньше кода, более четко выражает мои намерения и не требует от меня особых познаний во внутреннем устройстве StructureMap. Как автору StructureMap мне нужен простой способ создания и тестирования функциональности малыми модулями, а это сравнительно трудно сделать из-за одного только наличия переменного интерфейса.

При подходе с использованием семантической модели я смог довольно легко создать функциональные классы и выполнить их модульное тестирование. Сам DSL-код становится очень простым, потому что он лишь настраивает семантическую модель.

Такое разделение выразительности DSL и семантической модели оказалось очень выгодным. Вам придется часто изменять синтаксис своего DSL, чтобы добиться написания простых и читаемых выражений с учетом обратной связи от ваших пользователей. Очередная итерация пройдет гораздо безболезненнее, если вам не понадобится беспокоиться о нарушении функциональности периода выполнения при изменении синтаксиса.

С другой стороны, применив DSL как официальный API для StructureMap, я уже в нескольких ситуациях смог расширить или реструктуризовать внутреннюю семантическую модель, не нарушая синтаксис DSL. Вот вам еще один пример, демонстрирующий преимущества принципа "Separation of Concerns" (разделения обязанностей) в проектировании ПО.

Переменные интерфейсы и формирователи выражений

Переменный интерфейс (fluent interface) — это API, в котором используется объединение методов в цепочку для создания лаконичного и легко читаемого синтаксиса. Полагаю, что наиболее известный пример — популярная библиотека jQuery для разработок на JavaScript.

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

Переменный интерфейс позволяет мне "уплотнить" код в меньшие блоки текста, потенциально упрощая его восприятие. Кроме того, он часто помогает пользователям моих API делать правильный выбор. Самый простой и, вероятно, наиболее распространенный прием в создании переменного интерфейса — заставить объект возвращать себя из вызовов метода (именно так в основном и работает jQuery).

У меня есть простой класс HtmlTag, используемый в StoryTeller для генерации HTML. Я могу быстро сформировать объект HtmlTag с помощью объединения методов в цепочку:

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

На внутреннем уровне объект HtmlTag возвращает себя из вызовов Text и AddClass:

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

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

В более сложных случаях вы можете разделить переменный интерфейс на две части: семантическую модель (обеспечивает поведение периода выполнения — подробнее об этом шаблоне позже) и серию классов Expression Builder, реализующих грамматику DSL.

Я использую пример этого шаблона в StoryTeller UI для определения комбинаций клавиш и динамических меню. Мне нужен был быстрый программный способ определения комбинации клавиш для какого-либо действия в UI. Кроме того, поскольку большинство не может запомнить все комбинации клавиш для каждого применяемого приложения, я хотел создать специальное меню в UI, где перечислялись бы все доступные комбинации клавиш и назначенные им действия. Вдобавок, так как экраны активируются в основной области вкладок StoryTeller UI, было желательно добавлять кнопки полоски динамического меню в UI, специфичный для активного экрана.

Конечно, я мог бы просто закодировать это "в лоб", используя Windows Presentation Foundation (WPF), но это означало бы необходимость редактирования нескольких разных областей XAML-разметки для клавиатурных жестов (keyboard gestures), команд, объектов полоски меню (menu strip) для каждого экрана и элемента меню. А потом еще пришлось бы связывать все это воедино. Вместо этого я хотел сделать регистрацию новых комбинаций клавиш и элементов меню предельно декларативной и сократить "поверхность" модифицируемого кода до единственной точки. Разумеется, я создал переменный интерфейс, который обеспечивает автоматическую настройку всех разрозненных WPF-объектов "за кулисами".

На практике я могу указать общую комбинацию клавиш для открытия экрана "Execution Queue" со следующим кодом:

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

В кода активации каждого экрана я могу определить временные комбинации клавиш и динамические меню для основной оболочки приложения:

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;

Теперь рассмотрим реализацию этого переменного интерфейса. За ним стоит класс семантической модели ScreenAction, который выполняет реальную работу, связанную с созданием всех необходимых WPF-объектов. Этот класс выглядит так:

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);
}

Важный момент: я могу создавать и тестировать объект ScreenAction независимо от переменного интерфейса, и теперь этот интерфейс просто конфигурирует объекты ScreenAction. DSL реализуется в классе ScreenObjectRegistry, который отслеживает список активных объектов ScreenAction (рис. 1).

Рис. 1 DSL реализуется в классе 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
 }

Регистрация нового действия экрана (screen action) начинается с вызова метода Action(name). При этом возвращается новый экземпляр класса BindingExpression, который действует как Expression Builder для настройки нового объекта ScreenAction и который частично показан на рис. 2.

Рис. Класс BindingExpression выступает в роли Expression Builder

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;
    }
}

Один из важных факторов во многих переменных интерфейсах — попытка руководить пользователем API, чтобы он делал вещи в определенном порядке. В случае, показанном на рис. 2, я использую интерфейсы BindingExpression для управления вариантами выбора для пользователей в IntelliSense, хотя я всегда возвращаю один и тот же объект BindingExpression. Подумайте об этом. Пользователи данного интерфейса должны указывать имя действия и комбинацию клавиш только раз. После этого пользователь больше не должен видеть соответствующие методы в IntelliSense. DSL-выражение начинает с вызова ScreenObjectRegistry.Action(name), получающего описательное название комбинации клавиш, которое появится в меню, и возвращающего новый объект BindingExpression как следующий интерфейс:

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

Приведение BindingExpression к IActionExpression оставляет пользователю единственный выбор — указать комбинацию клавиш, что вернет тот же объект BindingExpression, но приведенный к интерфейсу IBindingExpression, а это позволит указать лишь единственное действие:

// 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);
}

Инициализаторы объектов

Теперь, когда мы ознакомились с объединением методов в цепочки и узнали, что этот процесс является основным в разработке внутреннего DSL на C#, рассмотрим альтернативные шаблоны, которые зачастую упрощают всю механику для разработчика DSL. Первая альтернатива — использование функционала инициализатора объектов, введенного в Microsoft .NET Framework 3.5.

Я до сих пор помню свою первую попытку в создании переменных интерфейсов. В то время я работал над системой, которая выступала в роли посредника в обмене сообщениями между клиентами и адвокатскими конторами, передающими счета в электронном виде. Одним из типичных вариантов применения была отправка сообщений клиентам от имени адвокатской конторы. С этой целью мы вызывали следующий интерфейс:

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

передаются всего три строковых аргумента. При его использовании проблема была в том, чтобы определять, куда посылается тот или иной аргумент. Да, утилиты вроде ReSharper могут показывать, какой параметр вы указываете в любой конкретный момент, но как быть со сканированием вызовов SendMessage при простом чтении кода? Посмотрите на следующий пример и вы сразу поймете, почему я так волновался насчет ошибок, связанных с неверным порядком строковых аргументов:

// 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");
 }

На том этапе я решил проблему удобства использования этого API, перейдя на переменный интерфейс, который более четко показывал, к чему относится каждый аргумент:

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

Я искренне считал, что благодаря этому API стал бы более удобным в использовании и менее подверженным ошибкам, но давайте посмотрим, как могла бы выглядеть нижележащая реализация формирователей выражений (expression builders) (рис. 3).

Рис. Реализация Expression Builder

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);
    }
}

К счастью, теперь есть другая альтернатива — инициализаторы объектов (object initializers) (либо именованные параметры в .NET Framework 4 или VB.NET). Создадим другую версию отправителя сообщений, которая принимает в качестве параметра единственный объект:

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
    }
}

А вот так мы используем API с применением инициализатора объектов:

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

Согласитесь, что третья инкарнация API уменьшает вероятность ошибок при использовании по сравнению с версией на основе переменного интерфейса.

Здесь вся штука в том, что переменный интерфейс — не единственный шаблон для создания более понятных API в .NET Framework. Этот подход гораздо больше распространен в JavaScript, где можно использовать нотацию JSON (JavaScript Object Notation) для полного определения объектов в одной строке кода, и в Ruby, для которого характерно применение хешей "имя-значение" в качестве аргументов методов.

Вложенное замыкание

Думаю, что многие считают, будто создавать DSL на C# можно только с помощью переменных интерфейсов и объединения методов в цепочку. Я тоже так считал в свое время, но с тех пор обнаружил другие методики и шаблоны, которые зачастую намного легче реализовать, чем объединение методов в цепочку. Один из завоевывающих все большую популярность шаблонов — вложенное замыкание (nested closure):

выражайте подэлементы вызова функции, помещая их в замыкание в аргумент.

Все чаще проекты веб-разработок с применением .NET выполняются на основе шаблона Model-View-Controller (MVC). Один из побочных эффектов этого — значительно большая потребность в генерации HTML-фрагментов в коде для элементов ввода. Манипуляции над строками для генерации HTML могут быстро выйти боком. Все кончится тем, что вы будете делать кучу вызовов для "санации" HTML, чтобы избежать атак с внедрением кода, а во многих случаях в ряде классов и методов может понадобиться какая-то дополнительная обработка конечного HTML-представления. Я хочу выразить создание HTML простой фразой вроде "мне нужен тег div с таким-то текстом и таким-то классом". Чтобы упростить такую генерацию HTML, мы моделируем HTML с помощью объекта HtmlTag, который используется примерно так:

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

что приводит к созданию следующего HTML:

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

В основе этой модели генерации HTML лежит объект HtmlTag, методы которого позволяют программно структуру HTML-элемента, например:

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);
}

Эта модель также позволяет добавлять вложенные HTML-теги:

[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>"
    );
}

На практике я часто обнаруживаю, что мне хочется одним махом добавить полностью сконфигурированный дочерний тег. Как уже упоминалось, я веду проект StoryTeller с открытым исходным кодом, который используется моей группой для выражения приемочных тестов. Часть функциональности StoryTeller заключается в выполнении всех приемочных тестов в процессе сборки с непрерывной интеграцией (continuous integration build) и создании отчета о результатах тестирования. Сводка результатов теста выражается в виде простой таблицы с тремя столбцами. Эта сводная таблица в HTML выглядит так:

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

Используя описанную выше модель HtmlTag, я генерирую структуру заголовка таблицы результатов с помощью следующего кода:

// _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");
});

Вызывая _table.Add, я передаю лямбда-функцию, которая полностью определяет, как генерировать первую строку заголовка. Применение шаблона вложенного замыкания позволяет мне передавать спецификацию без предварительного создания другой переменной для тега tr. Поначалу вам может не понравиться этот синтаксис, но он делает более четким ваш код. На внутреннем уровне метод Add, использующий вложенное замыкание, представляет собой вот что:

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;
}

В качестве еще одного примера основной StructureMap-класс Container инициализируется передачей вложенного замыкания, которое представляет все необходимые настройки для контейнера:

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>();
        });
});

Сигнатура и тело этой функции-конструктора выглядят так:

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);
}

В данном случае шаблон вложенного замыкания нужен мне по двум причинам. Первая — контейнер StructureMap одним махом принимает полную конфигурацию, затем с помощью Reflection.Emit динамически генерирует объекты-"формирователи" до использования контейнера. Прием конфигурации в виде вложенного замыкания позволяет получать всю конфигурацию за раз и автоматически выполнять генерацию до того, как контейнер становится доступным для использования. Вторая причина заключается в отделении методов регистрации типов в контейнере во время конфигурирования от методов, с помощью которых в период выполнения вы получаете сервисы (это пример Interface Segregation Principle, буква "I" в S.O.L.I.D.).

Я включил шаблон вложенного замыкания в эту статью потому, что он начинает доминировать в проектах для .NET Framework с открытым кодом, таких как Rhino Mocks, Fluent NHibernate и многих утилит IoC. Кроме того, шаблон вложенного замыкания зачастую значительно проще в реализации, чем использование только объединения методов в цепочку. Его недостаток лишь в том, что многие разработчики до сих пор плохо разбираются в лямбда-выражениях. Более того, эта методика практически неприменима в VB.NET, так как этот язык не поддерживает многострочные лямбда-выражения.

IronRuby и Boo

Все примеры в статье написаны на C#, чтобы охватить более широкую аудиторию, но если вы заинтересованы в разработке DSL, присмотритесь к другим CLR-языкам. В частности, IronRuby исключительно удобен для создания внутренних DSL ввиду своей гибкости и сравнительно четкому синтаксису (необязательные скобки, никаких точек с запятыми и т. д.). В разработках DSL под CLR также популярен язык Boo.

Названия и определения проектировочных шаблонов взяты из онлайнового черновика будущей книги Мартина Фаулера (Martin Fowler) по языкам, специфичным для предметных областей (martinfowler.com/dslwip/index.html).

**Джереми Миллер (Jeremy Miller)**обладатель статуса Microsoft MVP в области C#, автор утилиты StructureMap с открытым исходным кодом (structuremap.sourceforge.net), предназначенной для введения зависимостей, и готовящейся к выпуску утилиты StoryTeller (storyteller.tigris.org) для тестирования по методике FIT в .NET. Посетите его блог "The Shade Tree Developer" по ссылке codebetter.com/blogs/jeremy.miller.

Выражаем благодарность за рецензирование этой статьи эксперту Гленну Блоку (Glenn Block).