1 de febrero de 2012

Cómo construir una interfaz fluent básica en 8 pasos

La versión en inglés de este post puede ser encontrada en http://blogs.southworks.net/jrowies/2012/02/01/how-to-build-a-basic-fluent-interface-in-8-steps/

Aclaración: el objetivo de este post no es mostrar una lista completa de las diferentes técnicas existentes para escribir una API fluent ya que en la web se puede encontrar mucha información al respecto. Si estás interesado en el tema, te recomiendo que leas el libro de Martin Fowler Domain-Specific Languages.

Ahora que me saqué ese peso de encima, empecemos a escribir una interfaz fluent.

Imaginemos que estamos desarrollando una API para publicar blog posts en múltiples plataformas (Blogger, WordPress, TypePad, etc.), es muy probable que nuestro (super simplificado) modelo del dominio sea algo similar a esto:



Entonces, crear un blog post debería ser algo así:

var post = new BlogPost();
post.Title = "How to build a basic fluent interface in 8 steps";
post.Body = "...";
var author = new Author(); // We should check the authors repository
                           // before creating a new one
author.Name = "John Doe";
author.Email = "johndoe@email.com";
author.Twitter = "@johndoe";
post.Author = author;
post.Tags = "Fluent API, Internal DSL";

Nuestro modelo del dominio es muy sencillo y el código necesario para crear un blog post es fácil de leer y escribir asi que, en este caso implementar una interfaz fluent podría ser un tanto innecesario; pero... es perfecto para que podamos aprender a escribir una API fluent. Comenzamos?
 
Paso 1 - Escribiendo un borrador de la interfaz

Escribí (en notepad) un borrador de la interfaz fluent mostrando cómo querés que se vea (no pienses demasiado acerca de como vamos a implementar la interfaz, sólo jugá un poco con las ideas)

Esto es lo que escribí yo:

var post = Post
  .Title("How to build a basic fluent interface in 8 steps")
  .Body("...")
  .Author()
    .Name("John Doe")
    .Email("johndoe@email.com")
    .Twitter("@johndoe")
  .Tags("Fluent API, Internal DSL");

Paso 2 - Usando un enfoque "test-first"

Una vez que encontraste una opción que te satisface, pasala a un test en Visual Studio y agregá el código que será necesario para validar el resultado generado por la interfaz fluent. Dependiendo del tamaño de la API, quizás sea recomendable hacer esto gradualmente. En este caso vamos a dejar de lado, por ahora, la parte del Autor.

En el siguiente código estamos creando dos instancias de BlogPost, la primera usando la API standard y la segunda usando nuestra nueva API fluent. Luego de que ambas instancias son creadas, se verifica su equivalencia usando el método CheckEquivalence. Este método debe verificar si ambas instancias de BlogPost tienen el mismo título, cuerpo y tags.
[TestCase]
public void ConfiguringPostWithoutAuthor()
{
    var expected = new BlogPost();
    expected.Title = "How to build a basic fluent interface in 8 steps";
    expected.Body = "...";
    expected.Tags = "Fluent API, Internal DSL";

    var post = Post
        .Title("How to build a basic fluent interface in 8 steps")
        .Body("...")
        .Tags("Fluent API, Internal DSL");
    BlogPost actual = post.Build();

    Assert.IsTrue(TestsHelper.CheckEquivalence(expected, actual));
}

Este test, por supuesto, aún ni compila.
 
Paso 3 - Comenzando a implementar la interfaz

Ahora, comencemos a implementar la clase Post. Observar que el método Title es estático para que pueda ser invocado sin necesidad de crear una instancia de la clase Post.
public class Post
{
    public static Post Title(string title)
    {
        var post = new Post();
        post.TitleValue = title;
        return post;
    }
}

Paso 4 - Agregando más métodos a la interfaz fluent

Una vez que tenemos nuestro punto de partida, continuemos agregando el resto de los métodos (esta vez como métodos de instancia en lugar de estáticos).
public class Post
{
    ...

    public Post Body(string body)
    {
        this.BodyValue = body;
        return this;
    }

    public Post Tags(string tags)
    {
        this.TagsValue = tags;
        return this;
    }
}

Devolver la misma instancia del objeto que estamos construyendo, luego de cada invocación a un método, es una de las técnicas mas comunes usadas para escribir interfaces fluent, la misma es llamada Method Chaining.
Paso 5 - Completando la primer iteración y corriendo tests

Ahora, implementemos el método Build creando una instancia de BlogPost con los valores provistos a través de la interfaz fluent.
public class Post
{
    ...

    public BlogPost Build()
    {
        var blogPost = new BlogPost();

        blogPost.Title = this.TitleValue;
        blogPost.Body = this.BodyValue;
        blogPost.Tags = this.TagsValue;

        return blogPost;
    }
}

Nuestro test debería estar en verde ahora!  

Paso 6 - No nos olvidemos del Autor

Acá viene la parte un poco complicada, agreguemos al Autor...

Si continuamos lo mismo que veníamos haciendo con los métodos Title, Body y Tags, deberíamos hacer que el método Author devuelva una instancia de la clase Post; pero si hacemos eso, tendríamos que agregar los métodos Name, Email y Twitter en la clase Post, pero eso no es algo que querramos hacer. Lo que queremos es tener esos métodos en una clase separada, así que en lugar de devolver una instancia de Post, vamos a devolver una instancia de una nueva clase (AuthorSpec). Veamos cómo sería.
public class Post
{
 ...

 public AuthorSpec Author()
 {
  var authorSpec = new AuthorSpec();
  return authorSpec;
 }
}

public class AuthorSpec
{
 public AuthorSpec Name(string name)
 {
  this.NameValue = name;
  return this;
 }

 public AuthorSpec Email(string email)
 {
  this.EmailValue = email;
  return this;
 }

 public AuthorSpec Twitter(string twitter)
 {
  this.TwitterValue = twitter;
  return this;
 }
}

Ahora creemos un nuevo test, agregando el código de "Author":
public void ConfiguringPostWithAuthor()
{
 var expected = new BlogPost();
 expected.Title = "How to build a basic fluent interface in 8 steps";
 expected.Body = "...";
 expected.Tags = "Fluent API, Internal DSL";
 var author = new Author();
 author.Name = "John Doe";
 author.Email = "johndoe@email.com";
 author.Twitter = "@johndoe";
 expected.Author = author;

 var post = Post
  .Title("How to build a basic fluent interface in 8 steps")
  .Body("...")
  .Author()
   .Name("John Doe")
   .Email("johndoe@email.com")
   .Twitter("@johndoe")
  .Tags("Fluent API, Internal DSL");
 BlogPost actual = post.Build();
  
 Assert.IsTrue(CheckEquivalence(expected, actual));
}

Esperá! El compilador dice que el método Tags no es reconocido como un miembro de la clase AuthorSpec. Eso es porque estamos invocando al método Tags sobre el valor de retorno del método Twitter, el cual es una instancia de AuthorSpec y no de Post.

Al escribir APIs fluent, es muy común encontrarse con este tipo de problemas, las soluciones más comunes a esto son terminar la especificación del Autor con un método "end", o pasar un "nested closure" al método Author.
Paso 7 - Cerrando la especificación de Author y corriendo más tests

Opción A, usando un método "End".
public class Post
{
    ...

    public AuthorSpec Author()
    {
        this.authorSpec = new AuthorSpec(this);
        return authorSpec;
    }
}

public class AuthorSpec
{
    public AuthorSpec(Post parent)
    {
        this.parent = parent;
    }

    ...

    public Post End()
    {
        return this.parent;
    }
}

Cómo se ve:
var post = Post
    .Title("How to build a basic fluent interface in 8 steps")
    .Body("...")
    .Author()
        .Name("John Doe")
        .Email("johndoe@email.com")
        .Twitter("@johndoe")
        .End()
    .Tags("Fluent API, Internal DSL");

Opción B, usando un "nested closure".
public class Post
{
    ...

    public Post Author(Action<AuthorSpec> spec)
    {
        this.authorSpec = new AuthorSpec();
        spec(authorSpec);
        return this;
    }
}

Cómo se ve:
var post = Post
    .Title("How to build a basic fluent interface in 8 steps")
    .Body("...")
    .Author(a => a
        .Name("John Doe")
        .Email("johndoe@email.com")
        .Twitter("@johndoe"))
    .Tags("Fluent API, Internal DSL");

Yo personalmente prefiero la opción B. Creo que es ligeramente más fácil de leer y el programador no tiene que recordar llamar al método "End".
Luego de implementar una de estas dos opciones, nuestro test debería dar verde.

Paso 8 - Entrando en terrenos peligrosos

Imaginemos que queremos forzar a la gente que utiliza nuestra API para que asignen el nombre del autor primero y luego configuren la dirección de correo ó la cuenta de twitter, pero no ambos (un "o exclusivo"). Esto se puede hacer usando interfaces y así mostrar sólo un subconjunto de los métodos a medida que se va armando la cadena de métodos.
public class Post
{
    ...

    public Post Author(Action<IAuthorSpecAfterCreation> spec)
    {
        this.authorSpec = new AuthorSpec();
        spec(authorSpec);
        return this;
    }
}

public interface IAuthorSpecAfterCreation
{
    IAuthorSpecAfterName Name(string name);
}

public interface IAuthorSpecAfterName
{
    IAuthorSpecAfterEmailOrTwitter Email(string email);
    IAuthorSpecAfterEmailOrTwitter Twitter(string twitter);
}

public interface IAuthorSpecAfterEmailOrTwitter
{
}

public class AuthorSpec : IAuthorSpecAfterCreation, 
  IAuthorSpecAfterName, IAuthorSpecAfterEmailOrTwitter
{
    public IAuthorSpecAfterName Name(string name)
    {
        this.NameValue = name;
        return this;
    }

    public IAuthorSpecAfterEmailOrTwitter Email(string email)
    {
        this.EmailValue = email;
        return this;
    }

    public IAuthorSpecAfterEmailOrTwitter Twitter(string twitter)
    {
        this.TwitterValue = twitter;
        return this;
    }
}
Esta técnica puede volverse muy útil, pero la complejidad de la API puede crecer rápidamente con tantas interfaces aquí y allá... es recomendable usarlo con precaución.
Los fuentes completos pueden ser tomados desde acá.

Eso es todo, espero que sea útil :)

No hay comentarios:

Publicar un comentario