Fluent Interfaces y TDD

Desarrollar con TDD al principio no es nada fácil pero luego se vuelve “la manera” de desarrollar. Ahora, no siempre hago TDD, si quiero probar algo tan solo tiro las lineas y listo pero, por otro lado, si quiero hacer algo bien por más que tenga algo de código hecho lo tiro y lo comienzo de nuevo con TDD (nunca he perdido mucho. Por el contrario, lo hago porque veo una diferencia).

El asunto es que diseñé una interface fluida para encapsular todos los detalles indeseables de la manipulación de documentos en el proyecto en el que trabajo actualmente. Veamos como se ve una sentencia:

var createdOn = new DateTime(2005, 1, 1);

 

Dh.Using.AnimalControl

    .Where(

        ac => ac["Created"].LessThan(createdOn),

        ac => ac["Sex"].Not.EqualTo("Unknown") )

    .Do( ac => ac.Document.ArchivedDate = DateTime.Now )

    .Archive();

Esta simple “sentencia” hace lo siguiente: invoca al servicio de consultas para solicitar los Ids de todos los documentos del módulo “Animal Control” creados con anterioridad al 1/1/2005 y con sexo conocido, luego, por cada uno de los Ids, utilizando el servicio de documentos, obtiene uno por uno de ellos (son documentos XML) y setea el elemento /Document/ArchivedDate con la fecha actual y, luego de verificar si el usuario tiene privilegios para editar,  lo “archiva” en otro repositorio utilizando el mismo servicio de documentos.

Bien, una vez terminada la prueba de concepto que asegurase que todas, o la mayoría, de las operaciones se iban a poder expresar utilizando este nuevo “DSL interno”, me decido a tirarlo y comenzarlo seriamente con TDD. Así que aprovecho para mostrar esto que suele entenderse mal. Veamos como NO se prueban:

[Test]

public void It_should_allow_us_manipulate_set_of_documents()

{

    var createdOn = new DateTime(2005, 1, 1);

 

    Dh.Using.AnimalControl

        .Where(

            ac => ac["Created"].LessThan(createdOn),

            ac => ac["Sex"].Not.EqualTo("Unknown") )

        .Perform( ac => ac.Document.ArchivedDate = DateTime.Now )

        .Archive();

 

    Assert.That("?", Is.EqualTo("?"), "?");

}

¿Cómo se escribe el Assert?  ¿Cómo le inyecto las dependencias? ¿Qué es lo que debo probar? ¿Hay algo mal aquí?. Para responder estas preguntas, debemos entender como se compone una interface fluida:

  1. El Expression Builder, como la llama Martin Fowler, es el que hace las veces de parser (en el sentido de que guia la sintaxis del lenguaje) y por lo general se compone de un conjunto de interfaces, diseñadas para posibilitarle al motor de intellisense que asista al programador en la escritura de las sentencia y, al menos una clase que implementa esas interfaces, que colecta la información de los “terminales”.  Esta o estas clases, aparte de especificar la sintaxis de la interface, son las encargadas de ir colectando la información sobre  “lo que queremos hacer”. Estas clases no tienen comportamiento significativo, son básicamente estructuras de datos con métodos y propiedades que almacenan valores y que por lo general (obviamente no siempre) retornan this.
  2. El Semantic Model, también de Fowler, es la estructura de datos en la que se almacena esa información sobre lo que “queremos hacer”, recolectada por el Expression Builder. Esta es la principal entrada para el intérprete o generador de código.
  3. El ejecutor. En realidad, en el contexto de una interface fluida (o DSL interno) no hablamos de generadores de código ni intérpretes, en su lugar tendremos un componente que ejecuta las acciones necesarias para complacernos. Este es el “ejecutor”, el que hace las cosas, el que tiene dependencias, el que puede requerir fakes, el que debe implementar la lógica real.

Visto así, podemos decir que el Expression Builder no es sino una amistosa y sintácticamente conveniente manera de completar, de llenar el modelo semántico (Semantic Model) el cual es la entrada principal para el “ejecutor”.

Entonces… ¿como se escriben las pruebas unitarias de una interface fluida?. La respuesta ahora es obvia: se prueban sus componentes (o unidades) por separado. Aquí también podemos hacerlo de manera top-down o bien bottom-up dependiendo si nos sentimos más cómodos creando primero las especificaciones del parser (Expression Builder) o bien del Ejecutor. Personalmente prefiero pensar la sintaxis primero. El pensar la sintaxis primero ayuda a conocer cuales van a ser los requerimientos los cuales no tienen detalles importantes a definir.

Martin Fowler le puso nombre a otra cosa que todos conocemos bien desde siempre pero que nunca se nos hubiese cruzado por la cabeza el bautizar, esto es: el ruido sintáctico. Bien, una interface fluida debe buscar el menor ruido sintáctico posible! por eso la propiedad “Using”  es global a Dh, para no tener que recurrir al keyword new, por la misma razón (para no introducir ruido) las dependencias no están presentes en la sintaxis. Estos parecen detalles pero el diablo está en ellos. Este es el esqueleto (incompleto) del expression builder de este dsl:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using ProofOfConcept;

 

namespace DocumentLanguage

{

    public class Dh : IModules, IActionable, ISelector 

    {

        private DhSemanticModel semanticModel;

 

        private Guid lastModule;

 

        private Dh()

        {

            semanticModel = new DhSemanticModel();

        }

 

        public static IModules Using

        {

            get

            {

                return new Dh();

            }

        }

 

        public ISelector CallForService

        {

            get

            {

                this.lastModule = WellKnownModules.CallForServiceModuleId;

                return this;

            }

        }

 

        public ISelector CaseFolder

        {

            get

            {

                this.lastModule = WellKnownModules.CaseFolderModuleId;

                return this;

            }

        }

 

        public ISelector CaseReport

        {

            get

            {

                this.lastModule = WellKnownModules.CaseReportModuleId; 

                return this;

            }

        }

 

        public ISelector AnimalControl

        {

            get

            {

                this.lastModule = WellKnownModules.AnimalControlModuleId;

                return this;

            }

        }

 

        public IActionable Perform(Action<dynamic> action)

        {

            semanticModel.RegisterPerformAction(action);

            return this;

        }

 

        public IActionable Id(params Guid[] numbers)

        {

            semanticModel.RegisterDocumentIdentities(this.lastModule, numbers);

            return this;

        }

 

        public IActionable Where(params Func<Fieldx, Comparison>[] condition)

        {

            var fx = new Fieldx();

            semanticModel.RegisterConditions(

                condition.ToList().ConvertAll(f => f(fx)));

            return this;

        }

 

        public void Save()

        {

            semanticModel.RegisterCommand("Save");

        }

 

        public void Seal()

        {

            semanticModel.RegisterCommand("Seal");

        }

 

        public void Archive()

        {

            semanticModel.RegisterCommand("Archive");

        }

 

        public void Delete()

        {

            semanticModel.RegisterCommand("Delete");

        }

 

        public IModules And

        {

            get 

            { 

                return this; 

            }

        }

 

        public IActionable DocumentExtension(string extensionName)

        {

            semanticModel.RegisterDocumentExtension(this.lastModule, extensionName);

            return this;

        }

    }

 

    public interface IModules

    {

        ISelector CallForService { get; }

        ISelector CaseReport { get; }

        ISelector CaseFolder { get; }

        ISelector AnimalControl { get; }

    }

 

    public interface ISelector

    {

        IActionable Id(params Guid[] numbers);

        IActionable Where(params Func<Fieldx, Comparison>[] condition);

        IActionable DocumentExtension(string extensionName);

    }

 

    public interface IActionable

    {

        IActionable Perform(Action<dynamic> action);

        void Save();

        void Seal();

        void Delete();

        void Archive();

        IModules And { get; }

    }

 

    public class Fieldx

    {

        public Field this[string fieldName]

        {

            get

            {

                return new Field(fieldName);

            }

        }

    }

 

    public class Field

    {

        private string name;

 

        public Field(string name)

        {

            this.name = name;

        }

 

        public Comparison LessThan (DateTime date)

        {

            return new Comparison();

        }

 

        public NegativeField Not

        {

            get

            {

                return new NegativeField(this);

            }

        }

    }

 

    public class NegativeField

    {

        private Field field;

 

        public NegativeField(Field field)

        {

            this.field = field;

        }

 

        public Comparison EqualTo(object value)

        {

            return new Comparison();

        }

    }

 

    public class Comparison

    {

        internal Comparison()

        {

        }

    }

}

El modelo semántico es igualmente sencillo.

Sin categoría

3 thoughts on “Fluent Interfaces y TDD

Responder a anonymous Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *