Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Lenguajes internos específicos de dominio
Jeremy Miller
Los Lenguajes específicos de dominio (DSL) han sido un tema popular durante los últimos dos años y su importancia probablemente crecerá en los próximos años. Es posible que ya esté siguiendo el proyecto “Oslo” (que ahora se llama Modelado de SQL Server) o experimentando con herramientas como ANTLR para implementar DSL “externos”. Una alternativa inmediatamente más accesible es crear DSL “internos” escritos dentro de un lenguaje de programación existente, como C#.
Es posible que quienes no sean desarrolladores encuentren que los DSL internos no son tan expresivos y legibles como los DSL externos que se pueden leer como inglés, pero la mecánica de crear un DSL interno es más simple porque no debe emplear compiladores o analizadores externos a su código.
Tenga en cuenta que no estoy sugeriendo que los DSL que aparecen en este artículo sean adecuados para que los expertos del negocio los revisen. En el caso de este artículo, sólo me enfocaré en la forma en que los patrones de los DSL internos pueden facilitar nuestro trabajo como desarrolladores al implementar API que son más fáciles de leer y escribir.
Estoy extrayendo muchos ejemplos de dos proyectos de código abierto escritos en C# que administro y desarrollo. El primero es StructureMap, una de las herramientas de contenedor de Inversión de control (IoC) para Microsoft .NET Framework. El segundo es StoryTeller, una herramienta de pruebas de aceptación. Puede descargar todo el código fuente de ambos proyectos a través de Subversion en el sitio web https://structuremap.svn.sourceforge.net/svnroot/structuremap/trunk o en storyteller.tigris.org/svn/storyteller/trunk (se requiere registro). También puedo sugerir el proyecto Fluent NHibernate fluentnhibernate.org como otra fuente de ejemplos.
Extensiones literales
Uno de los puntos más importantes que quiero establecer en este artículo es que existen algunos pequeños trucos que puede realizar para que la lectura del código sea más limpia y más expresiva. Estos trucos pequeños realmente pueden agregar valor a sus esfuerzos de codificación al hacer que el código sea más fácil de escribir correctamente a medida que se vuelve más declarativo y puede revelar más intenciones.
Con cada vez más frecuencia, uso métodos de extensión en objetos básicos como cadenas y números para reducir la repetición en las API de .NET Framework principales y para aumentar la legibilidad. Este patrón para ampliar objetos de valor se denomina “extensiones literales”.
Empecemos con un ejemplo sencillo. Mi proyecto actual implica reglas configurables para eventos programados y eventos recurrentes. Inicialmente intentamos crear un DSL interno pequeño para configurar estos eventos (ahora, por el contrario, estamos en el proceso de cambiar a un enfoque de DSL externo). Estas reglas dependen en gran medida de los valores TimeSpan en lo que respecta a la frecuencia con que debe repetirse un evento, cuándo debe comenzar y cuándo caduca. Puede verse de una manera similar a este fragmento de código:
x.Schedule(schedule =>
{
// These two properties are TimeSpan objects
schedule.RepeatEvery = new TimeSpan(2, 0, 0);
schedule.ExpiresIn = new TimeSpan(100, 0, 0, 0);
});
Ponga atención especialmente a “new TimeSpan(2, 0, 0)” y “new TimeSpan(100, 0, 0, 0)”. En su calidad de experimentado desarrollador de .NET Framework, puede analizar que esas dos partes de código significan “2 horas” y “100 días”, pero probablemente tuvo que pensarlo un poco, ¿no? En lugar de eso, hagamos que la definición de TimeSpan sea más legible:
x.Schedule(schedule =>
{
// These two properties are TimeSpan objects
schedule.RepeatEvery = 2.Hours();
schedule.ExpiresIn = 100.Days();
});
Todo lo que hice en el ejemplo anterior fue usar algunos métodos de extensión en el objeto entero que devuelve objetos 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);
}
}
En términos de implementación, cambiar de “new TimeSpan(2, 0, 0, 0)” a “2.Days()” no es un cambio tan grande pero, ¿cuál es más fácil de leer? Sé que cuando traduzco reglas empresariales a código, prefiero decir "dos días" en lugar de "un intervalo de tiempo que consta de dos días, cero horas y cero minutos". La versión más legible del código es más fácil de corregir, y para mí esa es una razón suficiente para usar la versión de expresión literal.
Modelo semántico
Cuando creo un DSL nuevo, necesito solucionar dos problemas. Primero, mi equipo y yo nos preguntamos cómo nos gustaría expresar el DSL de una manera lógica y autodescriptiva que lo haga fácil de usar. Tanto como sea posible, trato de hacerlo independiente de cómo se estructurará o creará la funcionalidad real.
Por ejemplo, la herramienta de contenedor de Inversión de control (IoC) de StructureMap permite que los usuarios configuren el contenedor de manera explícita dentro del "DSL de registro" de StructureMap, de la siguiente manera:
var container = new Container(x =>
{
x.For<ISendEmailService>().HttpContextScoped()
.Use<SendEmailService>();
});
Si todavía no está familiarizado con el uso de un contenedor de IoC, todo lo que ese código hace es establecer que cuando pida al contenedor en tiempo de ejecución un objeto de tipo ISendEmailService, obtendrá una instancia del tipo concreto SendEmailService. La llamada a HttpContextScoped dirige StructureMap a “centrar” los objetos ISendEmailService en una sola HttpRequest, lo que significa que si el código se ejecuta dentro de ASP.NET, habrá una sola instancia de ISendEmailService para cada solicitud HTTP individual, independiente de la cantidad de veces que solicite un ISendEmailService dentro de una sola solicitud HTTP.
Una vez que tengo una idea para la sintaxis deseada, me quedo con la pregunta crucial de cómo exactamente voy a conectar la sintaxis de DSL con el código que implementa el comportamiento real. Puede colocar el código de comportamiento directamente en el código de DSL, para que esas acciones de tiempo de ejecución sucedan directamente en los objetos del Generador de expresiones, pero no lo recomendaría a menos que sea en los casos más sencillos. Las clases del Generador de expresiones pueden ser algo difíciles para realizar una prueba unitaria, y la depuración pasando por una interfaz fluida no es favorable ni para la productividad ni para su sanidad mental. Usted en realidad quiere ponerse en una posición en la que pueda realizar pruebas unitarias (de preferencia), depurar y solucionar los problemas de los elementos de comportamiento de tiempo de ejecución de su DSL sin tener que pasar por todo el direccionamiento indirecto del código en una interfaz fluida típica.
Necesito crear el comportamiento de tiempo de ejecución y tengo que implementar un DSL que exprese el intento del usuario del DSL lo más limpiamente posible. En mi experiencia, ha sido muy útil separar el comportamiento de tiempo de ejecución en un “modelo semántico”, que Martin Fowler define como “El modelo de dominio rellenado por un DSL” martinfowler.com/dslwip/SemanticModel.html.
El punto clave acerca del fragmento de código anterior es que no hace ningún trabajo real. Todo lo que hace ese poco de código de DSL es configurar el modelo semántico del contenedor de IoC. Podría omitir la interfaz fluida anterior y crear los objetos del modelo semántico de la siguiente manera:
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);
El código de DSL de registro y el código que está directamente arriba son idénticos en el comportamiento de tiempo de ejecución. Todo lo que hace el DSL es crear el gráfico de objetos de los objetos PluginGraph, PluginFamily, Instance y HttpContextLifecycle. Entonces la pregunta es... ¿por qué molestarse con tener dos modelos separados?
Antes de nada, como usuario definitivamente quiero la versión de DSL de los dos ejemplos de código anteriores, porque es mucho menos código que escribir, expresa más limpiamente mi intento y no requiere que el usuario conozca mucho acerca de la información interna de StructureMap. Como implementador de StructureMap, necesito una forma fácil para crear y probar la funcionalidad en unidades pequeñas, y eso es algo relativamente difícil de hacer con una interfaz fluida por sí sola.
Con el enfoque del modelo semántico, pude crear y realizar una prueba unitaria de las clases de comportamiento con bastante facilidad. El mismo código de DSL pasa a ser muy simple, porque todo lo que hace es configurar el modelo semántico.
Esta separación de la expresión de DSL y el modelo semántico ha resultado muy beneficiosa con el tiempo. Con frecuencia tendrá que realizar una iteración de algún elemento con la sintaxis de su DSL para alcanzar expresiones más legibles y editables basadas en los comentarios a partir del uso. Esa iteración será mucho más imperceptible si no tiene que preocuparse mucho de dividir la funcionalidad de tiempo de ejecución al mismo tiempo en que cambia la sintaxis.
Por otro lado, al tener el DSL como la API oficial para StructureMap, en varias ocasiones he podido ampliar o reestructurar el modelo semántico interno sin dividir la sintaxis de DSL. Éste es sólo un ejemplo más de los beneficios del principio de “Separación de preocupaciones” en el diseño de software.
Interfaces fluidas y generadores de expresiones
Una interfaz fluida es un estilo de API que usa encadenamiento de método para crear una sintaxis breve y legible. Creo que el ejemplo más conocido es probablemente la cada vez más popular biblioteca jQuery para programación JavaScript. Los usuarios de jQuery reconocerán rápidamente código como el siguiente:
var link = $(‘<a></a>’).attr("href", "#").appendTo(binDomElement);
$(‘<span></span>’).html(binName).appendTo(link);
Una interfaz fluida me permite intensificar la densidad del código en una pequeña ventana de texto, lo que puede hacer que el código sea más fácil de leer. Además, a menudo me ayuda a guiar al usuario de mis API para seleccionar las opciones adecuadas. El truco más simple y quizás el más común para crear una interfaz fluida es simplemente hacer que un objeto se devuelva a sí mismo desde las llamadas al método (que es, a grandes rasgos, cómo funciona jQuery).
Tengo una clase simple que uso en StoryTeller para generar HTML que se denomina "HtmlTag". Puedo desarrollar rápidamente un objeto HtmlTag con un encadenamiento de método de la siguiente manera:
var tag = new HtmlTag("div").Text("my text").AddClass("collapsible");
De manera interna, el objeto HtmlTag sólo se devuelve a sí mismo a partir de las llamadas a Text y AddClass:
public HtmlTag AddClass(string className)
{
if (!_cssClasses.Contains(className))
{
_cssClasses.Add(className);
}
return this;
}
public HtmlTag Text(string text)
{
_innerText = text;
return this;
}
En un escenario más complicado, puede separar la interfaz fluida en dos partes: el modelo semántico que suministra el comportamiento de tiempo de ejecución (luego mencionaremos más información sobre este patrón) y una serie de clases del “Generador de expresiones” que implementan las gramáticas de DSL.
Uso un ejemplo de este patrón en la interfaz de usuario de StoryTeller para definir accesos directos del teclado y menús dinámicos. Deseaba una rápida manera programática para definir un acceso directo de teclado para una acción en la interfaz de usuario. Además, como la mayoría de nosotros no puede recordar cada acceso directo de teclado para cada aplicación que usamos, deseaba crear un solo menú en la interfaz de usuario que mostrara todos los accesos directos disponibles y las combinaciones del teclado para ejecutarlas. Además, como las pantallas están activadas en el área de fichas principal de la interfaz de usuario de StoryTeller, deseaba agregar botones de la franja de menús dinámicos a la interfaz de usuario que fuesen específicos para la pantalla activa.
Ciertamente podría haber codificado esto en la manera idiomática de Windows Presentation Foundation (WPF), pero podría haber significado editar un par de áreas distintas del marcado de XAML para gestos del teclado, comandos, los objetos de la franja de menús para cada pantalla y elementos de menú, y luego asegurarse de que estos se relacionen entre sí correctamente. En lugar de eso, quise hacer que este registro de nuevos accesos directos y elementos de menú fueran lo más declarativos posible, y quise reducir el área de superficie del código a un solo punto. Por supuesto, creé una interfaz fluida que configuraba por mí todos los objetos de WPF dispares en segundo plano.
En el uso, puedo especificar un acceso directo global para abrir la pantalla “Cola de ejecución” con el siguiente código:
// Open the "Execution Queue" screen with the
// CTRL - Q shortcut
Action("Open the Test Queue")
.Bind(ModifierKeys.Control, Key.Q)
.ToScreen<QueuePresenter>();
En el código de activación de la pantalla para una pantalla individual, puedo definir accesos directos de teclado temporales y las opciones del menú dinámico en la shell de aplicación principal con un código similar al siguiente:
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;
Ahora examinemos la implementación de esta interfaz fluida. Subyacente se encuentra una clase de modelo semántico llamada ScreenAction que realiza el trabajo real de crear todos los objetos de WPF componentes. Esta clase se parece a:
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);
}
Este es un detalle importante. Puedo crear y probar el objeto ScreenAction independientemente de la interfaz fluida, y ahora la interfaz fluida sólo debe configurar objetos ScreenAction. El DSL real está implementado en una clase llamada ScreenObjectRegistry que hace un seguimiento de la lista de objetos ScreenAction activos (consulte la figura 1).
Figura 1 DSL está implementado en 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
}
El registro de una nueva acción de pantalla comienza con la llamada al método Action(name) anterior y devuelve una instancia nueva de la clase BindingExpression que actúa como un Generador de expresiones para configurar el nuevo objeto ScreenAction, que aparece de manera parcial en la figura 2.
Figura 2 Clase BindingExpression que actúa como Generador de expresiones
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;
}
}
Uno de los factores importantes en varias interfaces fluidas es tratar de guiar al usuario de la API para que realice las cosas en cierto orden. En el caso que se presenta en la figura 2, uso interfaces en BindingExpression estrictamente para controlar las opciones del usuario en IntelliSense, incluso si siempre devuelvo el mismo objeto BindingExpression. Piense en lo siguiente. Los usuarios de esta interfaz fluida sólo deben especificar una vez el nombre de la acción y las teclas de acceso directo de teclado. Después de eso, el usuario no debiera ver esos métodos en IntelliSense. La expresión de DSL comienza con la llamada a ScreenObjectRegistry.Action(nombre), que captura el nombre descriptivo del acceso directo que aparecerá en los menús y devuelve un nuevo objeto BindingExpression, como esta interfaz:
public interface IActionExpression
{
IBindingExpression Bind(Key key);
IBindingExpression Bind(ModifierKeys modifiers, Key key);
}
Al convertir BindingExpression en IActionExpression, la única opción que tiene el usuario es especificar las combinaciones de teclas para el acceso directo, con lo que se devolverá el mismo objeto BindingExpression, pero convertido en la interfaz IBindingExpression que sólo permite que los usuarios especifiquen una sola acción:
// 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);
}
Inicializadores de objetos
Ahora que hemos incorporado el encadenamiento de método como el pilar del desarrollo de DSL interno en C#, comencemos a examinar los patrones alternativos que a menudo pueden llevar a mecanismos más simples para el desarrollador de DSL. La primera alternativa simplemente es usar la funcionalidad del inicializador de objetos incorporada en Microsoft .NET Framework 3.5.
Todavía me acuerdo de mi primera incursión en las interfaces fluidas. Yo trabajaba en un sistema que actuaba como un broker de mensajes entre bufetes de abogados que envían electrónicamente facturas legales y sus clientes. Uno de los casos de uso comunes para nosotros era enviar mensajes a los clientes en nombre de los bufetes de abogados. Para enviar los mensajes, invocábamos una interfaz similar a la siguiente:
public interface IMessageSender
{
void SendMessage(string text, string sender, string receiver);
}
Ésta es una API muy simple; sólo se introducen tres argumentos de cadena y listo. El problema en el uso es saber qué argumento va aquí. Sí, herramientas como ReSharper le pueden mostrar el parámetro que está especificando en cada ocasión, pero... ¿cómo explorar las llamadas a SendMessage cuando sólo está leyendo código? Mire el uso del siguiente ejemplo de código y comprenderá exactamente a lo que me refiero cuando hablo de errores que surgen al cambiar el orden de los argumentos de cadena:
// 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");
}
En ese momento, solucioné el problema del uso de la API cambiándome a un enfoque de interfaz fluida que indicaba con mayor claridad qué argumento era cuál:
public void SendMessageFluently(FluentMessageSender sender)
{
sender
.SendText("the message body")
.From("PARTNER001").To("PARTNER002");
}
Sinceramente creí que esto sería una API más práctica y menos proclive a los errores, pero miremos cómo podría verse la implementación subyacente de los generadores de expresiones en la figura 3.
Figura 3 Implementación de un generador de expresiones
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);
}
}
Es mucho más código para crear la API que se requería originalmente. Afortunadamente, ahora tenemos otra alternativa con inicializadores de objetos (o con parámetros denominados en .NET Framework 4 o VB.NET). Creemos otra versión del remitente del mensaje que tome un objeto único como su parámetro:
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
}
}
Ahora, el uso de la API con un inicializador de objetos es:
public void SendMessageAsParameter(ParameterObjectMessageSender sender)
{
sender.Send(new SendMessageRequest()
{
Text = "the message body",
Receiver = "PARTNER001",
Sender = "PARTNER002"
});
}
Posiblemente, esta tercera encarnación de la API reduce los errores en el uso con mecanismos mucho más simples que la versión de interfaz fluida.
Aquí el punto es que las interfaces fluidas no son el único patrón para crear API más legibles en .NET Framework. Este enfoque es mucho más común en JavaScript, en el que puede usar notación de objetos JavaScript (JSON) para especificar completamente los objetos en una línea de código, y en Ruby, donde es idiomático usar hashes de nombre/valor como argumentos para los métodos.
Cierre anidado
Creo que muchas personas suponen que las interfaces fluidas y el encadenamiento de método son las únicas posibilidades para crear DSL dentro de C#. Yo también creía eso, pero he encontrado otras técnicas y patrones que con frecuencia son mucho más fáciles de implementar que el encadenamiento de método. Un patrón cada vez más popular es el patrón de cierre anidado:
Exprese los subelementos de la instrucción de una llamada de función colocándolos en un cierre de un argumento.
Cada vez más proyectos de desarrollo web con .NET se realizan con el patrón Controlador de vista de modelo. Uno de los efectos secundarios de este cambio es una necesidad mucho mayor de generar fragmentos de código de HTML en código para elementos de entrada. La manipulación de cadenas directa para generar el código HTML puede llegar a ser muy rápida. Termina repitiendo muchas llamadas para “sanear” el código HTML para evitar ataques por inyección y, en muchos casos, es posible que deseemos permitir que varias clases o métodos tengan algo que decir en la representación del código HTML final. Quiero expresar la creación de código HTML diciendo sólo que “quiero una etiqueta div con este texto y esta clase". Para facilitar esta generación de código HTML, modelamos el HTML con un objeto "HtmlTag" que, en la práctica, se ve similar a lo siguiente:
var tag = new HtmlTag("div").Text("my text").AddClass("collapsible");
Debug.WriteLine(tag.ToString());
lo que genera el siguiente código HTML:
<div class="collapsible">my text</div>
El núcleo de este modelo de generación de código HTML es el objeto HtmlTag que tiene métodos para crear de manera programática una estructura de elemento HTML similar a la siguiente:
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);
}
Este modelo también nos permite agregar etiquetas HTML anidadas como:
[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>"
);
}
En el uso real, con frecuencia encuentro que deseo agregar una etiqueta secundaria completamente configurada en un paso. Tal como mencioné, tengo un proyecto de código abierto llamado StoryTeller que mi equipo usa para expresar pruebas de aceptación. Parte de la funcionalidad de StoryTeller es ejecutar todas las pruebas de aceptación en nuestra compilación de integración continua y generar un informe con los resultados de la prueba. El resumen de resultados de la prueba se expresa como una tabla simple con tres columnas. El aspecto del código HTML de la tabla de resumen es similar al siguiente:
<table>
<thead>
<tr>
<th>Test</th>
<th>Lifecycle</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<!-- rows for each individual test -->
</tbody>
</table>
Usando el modelo HtmlTag ya descrito, genero la estructura del encabezado de la tabla de resultados con este código:
// _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");
});
En la llamada a _table.Add, introduzco una función lambda que especifica completamente cómo generar la primera fila de encabezados. Usar el patrón de cierre anidado me permite introducir la especificación sin tener que crear primero otra variable para la etiqueta "tr". Puede que esta sintaxis no le guste a primera viste, pero hace que el código sea más breve. Internamente, el método Add que usa el cierre anidado es simplemente el siguiente:
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;
}
Para presentar otro ejemplo, la clase de contenedor principal StructureMap se inicializa al introducir un cierre anidado que representa toda la configuración deseada para el contenedor, de la siguiente manera:
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>();
});
});
La firma y el cuerpo de esta función de constructor es:
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);
}
Tuve un par de razones para usar el patrón de cierre anidado en este caso. La primera es que el contenedor StructureMap funciona realizando la configuración completa en un paso y luego usando Reflection.Emit para generar dinámicamente objetos de “generador” antes de poder usar el contenedor. Realizar la configuración a través de un cierre anidado me permite capturar toda la configuración de una vez y realizar discretamente la emisión justo antes de que el contenedor quede disponible para ser usado. La otra razón es separar los métodos para registrar tipos con el contenedor al momento de la configuración de los métodos que usaría en tiempo de ejecución para recuperar servicios (éste es un ejemplo del principio de separación de interfaces, que representa la "I" en S.O.L.I.D.).
Incluí el patrón de cierre anidado en este artículo porque es cada vez más común en proyectos de código abierto de .NET Framework como Rhino Mocks, Fluent NHibernate y muchas herramientas de IoC. Además, con frecuencia pienso que el patrón de cierre anidado es significativamente más fácil de implementar que usar sólo el encadenamiento de método. El inconveniente es que muchos desarrolladores todavía no se sienten cómodos con las expresiones lambda. Además, esta técnica apenas puede usarse en VB.NET porque VB.NET no admite expresiones lambda de varias líneas.
IronRuby y Boo
Todos los ejemplos que cité en este artículo están escritos en C# para que sean atractivos para el público en general, pero si le interesa desarrollar DSL, es posible que desee ver el uso con otros lenguajes CLR. En particular, IronRuby es excepcional para crear DSL internos gracias a su sintaxis flexible y casi completamente clara (paréntesis opcionales, sin punto y coma y muy breve). Incluso yendo más lejos, el lenguaje Boo también es popular para el desarrollo de DSL en el CLR.
Los nombres y las definiciones de los patrones de diseños están obtenidos del borrador en línea del próximo libro de Martin Fowler sobre los lenguajes específicos de dominio en el sitio web martinfowler.com/dslwip/index.html.
Jeremy Milleres MVP de Microsoft de C# y creador de la herramienta de StructureMap de código abierto (structuremap.sourceforge.net) para la inserción de dependencias con .NET, así como de la próxima herramienta StoryTeller (storyteller.tigris.org) para las pruebas mejoradas de FIT en .NET. Visite su blog, "The Shade Tree Developer" en codebetter.com/blogs/jeremy.miller, que forma parte del sitio CodeBetter.
Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Glenn Block