Haz que tu código fluya

Sin duda la expresividad, la facilidad para leerlo y tener clara su intención y efectos sobre el sistema, es una de las características más importantes del código. Si por algo son interesantes las interfaces fluentes son por que aumentan extraordinariamente.

Los primeros en describir formalmente esta técnica fueron Eric Evans y Martin Fowler, aunque el origen de la misma esta en el idiom, descrito para C++, method chaining. Quizás la primera librería que presenta un uso acusado de este idioma es la Iibrería iostream de C++ que permite construcciones de estilo de out << “hola” << “ “<< “mundo” << endl, aplicando method chaining al operador <<. Además en C++ se hace a menudo uso del named parameter idiom, que también está en la base de las fluent interfaces.

¿Cómo implementamos fluent interfaces?

La técnica es sumamente sencilla en cualquier lenguaje orientado a objetos. Se trata básicamente en crear métodos que además de realizar una labor, que generalmente es cambiar el estado del objeto sobre el que se invoca el método devolver como resultado de la función el propio objeto (this, en los lenguajes de la familia C) de manera que podamos encadenar la siguiente llamada.

Miremos el siguiente ejemplo:

File f = OpenFile("foo.txt")

        .ReadOnly()

        .CreateIfNotExist()

        .AppendWhenWriting()

        .BlockSize(1024)

        .Unbuffered()

        .ExclusiveAccess();

Si el método ReadOnly de la clase está implementado de esta manera:

OpenFile ReadOnly()

{

    _ReadOnly = true;

    return this;

}

…podemos encadenar la llamada a CreateIfNotExists sin problemas, dotando a nuestro código de una expresividad mayor que si simplemente hubiésemos hecho:

File f = OpenFile("foo.txt")

     f.ReadOnly = true;

     f.CreateIfNotExist = true;

     f.AppendWhenWriting = true;

     f.BlockSize = 1024;

     f.BufferMode = BufferMode.Unbuffered;

     f.AccessMode = AccessMode.ExclusiveAccess;

Vemos que implementar fluent interfaces es trivial en casi cualquier lenguaje orientado a objetos. Pero que sea fácil no quiere decir que sea adecuado siempre ¡ojo!. Ahora bien, cuando es adecuado, la potencia de esta técnica es clara y notoria. Esta técnica esta especialmente indicada cuando tenemos una serie de métodos y/o propiedades que tiene en la mayoría de las ocasiones tienen sentido invocar uno tras otro sobre el mismo objeto. Por ejemplo, una variación de esta técnica se usa para implementar las continuaciones en la Taks Parallel Library de .Net, permitiendo que podamos expresar de manera muy simple el encadenamiento de una serie de tareas:

new Task().ContinueWith(…)

          .ContinueWith(…)

          .ContinueWith(…)

          .ContinueWith(…)

          …

¿Cómo nos ayudan las fluen interfaces?

Hay dos librerías que son un claro ejemplo de la potencia de esta técnica y que son sumamente útiles.

La primera es una librería Fluent Validation que permite expresar validaciones de manera sumamente elegante:

using FluentValidation;

public class CustomerValidator: AbstractValidator<Customer> {
  public CustomerValidator() {
    RuleFor(customer => customer.Surname).NotEmpty();
    RuleFor(customer => customer.Forename).NotEmpty().WithMessage("Please specify a first name");
    RuleFor(customer => customer.Company).NotNull();
    RuleFor(customer => customer.Discount).NotEqual(0).When(customer => customer.HasDiscount);
    RuleFor(customer => customer.Address).Length(20, 250);
    RuleFor(customer => customer.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
  }

  private bool BeAValidPostcode(string postcode) {
    // custom postcode validating logic goes here
  }
}

Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();
ValidationResult results = validator.Validate(customer);

bool validationSucceeded = results.IsValid;
IList<ValidationFailure> failures = results.Errors;

La otra, Fluent Assertions, es una librería que permite expresar aserciones en nuestras pruebas unitarias utilizando una fluent interface:

// Example

string actual = "ABCDEFGHI";

actual.Should().StartWith("AB").And.EndWith("HI").And.Contain("EF").And.HaveLength(9);

 

// Example

IEnumerable collection = new[] { 1, 2, 3 };

collection.Should().HaveCount(4, "because we thought we put three items in the collection"))

 

// Example

var recipe = new RecipeBuilder()

   .With(new IngredientBuilder().For("Milk").WithQuantity(200, Unit.Milliliters))

   .Build();

 

// Act

Action action = () => recipe.AddIngredient("Milk", 100, Unit.Spoon);

 

// Assert

action

   .ShouldThrow<RuleViolationException>()

   .WithMessage("Cannot change the unit of an existing ingredient")

   .And.Violations.Should().Contain(BusinessRule.CannotChangeIngredientQuanity);

Ambas librerías están disponibles a través de NuPack, así que no tienes disculpa para no probarlas.

¡Un saludo!

6 comentarios en “Haz que tu código fluya”

  1. Yo tengo una molonidad parecida para el proyecto que estoy haciendo ahora, tengo una aplicacion que usa TPL para consumir rss, y para generar una tarea que parsee el rss y otra para que consuma el resultado y lo procese, tengo este codigo:

    item.BlogRss
    .CreateRssReader()
    .CreateTask
    ()
    .ContinueWith(task =>
    {
    SaveRssItems(task.Result, item.KeyPlayerId);
    }, TaskSchedulerOptions.CurrentTaskContinuationOptions);

    Que me permite hacer eso con método extensores.

    Y como siempre no hemos reparado en gastos.

    Saludos!

  2. @Christian: ¡Que placer verte por aquí! Gracias por el aporte, muy interesante.

    @Luis: por que hacerlo simple si lo puedes hacer que mole ¿eh?… coñas a parte, mola como lo has construido. ¡Canela fina! 😉

    ¡Un saludo!

  3. ¡Me encanta cuando enseñamos código fuente! Gracias Rodrigo.

    Respecto al ejemplo del RSS, Luis. ¿Conoces el patrón Builder? Se parece mucho a lo que escribes (aunque no es exactamente igual).

    Por último, Rodrigo, ya te lo he preguntado por twitter, pero sospecho que como buen vasco no tendrás suficiente con 140 caracteres para contestarme. 😉 ¿Qué diferencia hay entre una “fluent API” y un DSL? (Reconozco que no me he leido -aún- el libro de Fowler sobre DSLs)

    Un abrazo,
    JMB

  4. @jmbeas, yo no soy un experto en DSLs, pero me voy a aventurar… Yo entiendo un Domain Specific Language como algo mucho más amplios que un fluent interface o incluso que un fluent API.

    Un DSL además de un lenguaje o alguna manera de expresar realidades de un dominio suele incluir una serie de herramientas o asistentes que facilitan y automatizan tareas en el marco de ese dominio, igual que los lenguajes generalistas tienen IDEs.

    Si duda habrá dominios para los que tenga sentido implementar su DSL en base a, entro otras cosas, un conjunto de fluent interfaces.

    ¡Un saludo!

Deja un comentario

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