Reflexiones Fluent Api (I): Introducción

Reconozco que utilizar una librería con Fluent Api puede ser cómodo o según como se haga bastante incómodo, como quería dar una nueva cara a una librería que tengo pensé en hacerla con estilo Fluent Api , podía ser interesante de cara a la librería y también a su facilidad de uso.

De la creación de esa librería y de algunas otras llegué a estas conclusiones que indico aquí (sobre todo para que no se me olviden la próxima vez).

Definición

La definición de Fluent Api está clara en la Wikipedia, esto no quita que haya que tener en cuenta algunos elementos o características propias del lenguaje que se vayan a usar, también indicar que se hace una distinción entre Fluent Api y method chaining, en este caso voy a usar las dos juntas indistintamente pero es importante tener esa distinción en cuenta.

También hay que tener en cuenta que el objeto con el que se está trabajando en Fluent Api es interno a la librería y puede estar compuesto a su vez de otros objetos, la idea de Fluent Api es construir ese objeto interno de una forma correcta para su uso.

Nomeclatura

Es importante tener claro como se van a prefijar los elementos y ceñirse a esta nomenclatura, muchas librerías usan los suyos propios indico aquí un estilo de ejemplo que es el que usaré:

  • AddXXX: Cuando queremos añadir un nuevo elemento al objeto interno, por ejemplo el objeto interno tiene una lista y queremos añadir un nuevo elemento a esa lista:
  • WithXXX: Para añadir una propiedad o establecer un valor al objeto con el que estamos trabajando, puede ser el interno o un objeto dependiente de este.
  • IsXXX / HasXXX: Los dos están orientado a valores booleanos, es verdad que podríamos usar también WithXXX pero muchas veces a los valores booleanos se les da un tratamiento especial para remarcarlos. Si buscásemos una diferencia entre IsXXX y HasXXX podríamos hacer lo siguiente:
    • IsXXX: Para establecer un valor booleano en una propiedad, por ejemplo IsDeveloper, IsCEO.
    • HasXXX: Cuando necesitamos hacer una verificación para obtener esa información o la sacamos de una colección, por ejemplo HasChilds, HasDriverLicense.
  • UseXXX: UseXXX se usa generalmente cuando queremos definir un comportamiento, por ejemplo internamente tenemos varias formas de crear el objeto o de guardarlo y queremos usar una en concreto, también es muy útil para establecer configuraciones.
  • AndXXX: Cuando vamos a concatenar dos propiedades que se configuran por separado pero deben de ir juntas ya que dependen entre sí.

Estilo

En este sentido también hay algunas sugerencias, por ejemplo si la línea de código es bastante larga puede ser interesante separarla en varias líneas, por ejemplo:

var configuration = Builder.Create()
                       .AddElement()
                       .WithProperty()
                       .AndAnotherOne().Save() 

El último método que finaliza se puede colocar también en otra línea aparte o en la misma, lo que interesa remarcar principalmente es cada opción de creación en una línea separada, en este punto puede haber algún problema con los editores de código de cara al formateo y la tabulación pero con un poco de práctica a la hora de generar las líneas o cambiando las opciones de formateo de código es suficiente.

Con los parámetros sucede algo parecido, es posible que los métodos tengan muchos parámetros en ese caso un nivel de indentación adicional ayuda bastante, como se ha comentado dependiendo del editor de código no siempre el formateo de código resulta cómodo así que puede haber variaciones:

var configuration = Builder.Create()
                       .AddElement(
                            text: "Hello")
                       .WithProperty(
                            description: "big",
                            color: "green")
                       .AndAnotherOne().Save() 

Por norma general se intenta usar métodos en todos los casos aunque es verdad que a veces pueda resultar más legible usar una propiedad, realmente es raro tener métodos que no usen parámetros así que por homogeneidad es mejor usar siempre métodos.

No debería pero estas configuraciones pueden llegar a ser largas por lo que incluirlas dentro de un método de configuración en una clase de inicio de la aplicación suele ser la opción más cómoda que es básicamente el comportamiento de los métodos de creación de .Net Core.

Estructura de Fluent Api

Primero recordar lo que dice la propia definición de la Wikipedia sobre la creación de las interfaces:

  • El método de una interfaz siempre devuelve como parámetro la propia interfaz.
  • Siempre hay un método sin contexto final que termina el proceso de creación (Save(), Build(),…)
  • Se trabaja siempre sobre el mismo contexto (u objeto interno) que se construirá.

Voy a usar este ejemplo demostrativo, no pretende ser un ejemplo real simplemente está hecho para poder explicar varios conceptos en los siguientes capítulos.

Diagrama general

Hay dos interfaces, una se encarga de la gestión de usuarios y la otra de la gestión de la conexión, ambas están conectadas a través de una interfaz que representa la configuración de una base de datos.

Reflexiones Fluent Api (I): Introducción

Reflexiones Fluent Api (II): Métodos

Introducción

En esta sección voy a indicar algunas notas a la hora de crear los métodos, básicamente los métodos que se van a utilizar son los que se muestran en la interfaz, su finalidad es crear el objeto.

Esquema de interfaces

Vamos ahora con algunas recomendaciones:

  • Usar parámetros solo con tipos primitivos o proporcionados por el sistema, como int, DateTime, string…, a ser posible nullables, es posible usar objetos para los métodos y en algunos casos reduce la carga de parámetros pero en ese caso estaríamos dando demasiada información sobre la creación del objeto, información que puede cambiar en un futuro y rompería la interfaz, de hecho los enumerados de usarse deberían ser propios y no los mismos que puede usar el objeto interno.
public IConnectionBuilder WithServer(
    string name = "",
    int? port = null,
    Version? version = null)
{
    Database.Server = new Server();
    Database.Server.Name = name;
    Database.Server.Port = port;
    Database.Server.Version = version;
    return this;
}
  • Todos los parámetros tienen valores por defecto, salvo los estrictamente obligatorios para una correcta creación del objeto que por otro lado idealmente deberían ser los mínimos. o no existir.
    Esto es debido a que generalmente la configuración por defecto del objeto debería ser lo más correcta posible y solo debería interesar sobrescribir algunas propiedades concretas a través de Fluent Api, acercándonos al concepto de convención sobre configuración.
    Otra opción es crear sobrecargas pero para objetos relativamente complejos la combinatoria de todos los parámetros a través de sobrecargas se hace demasiado complicada.
IConnectionBuilder WithServer(
    string name = "",
    int? port = null,
    Version? version = null);
  • Todos los parámetros están documentados, puede parecer obvio pero en Fluent Api es fácil perder el hilo de que método se está usando y sobre que objeto está siendo aplicado así que indicar el uso y efecto es importante.
/// <summary>
/// This method sets which server will be used to connect to database.
/// </summary>
/// <param name="name">Server name or IP, by default localhost.</param>
/// <param name="port">Por to connect to server, by default 669.</param>
/// <param name="version">Driver version to use, by default none.</param>
IConnectionBuilder WithServer(
    string name = "localhost",
    int? port = 669,
    Version? version = null);
  • No usar parámetros como contadores, es decir si queremos añadir 3 objetos iguales es mejor llamar tres veces al método que pasar un 3 al método, internamente a nivel de código se maneja mejor y queda más clara la intención especialmente si hay que establecer diferentes valores en estos métodos, obviamente si el valor representa internamente un contador se puede saltar esta recomendación.
IUsersBuilder builder = DatabaseBuilder
    .Create()
    .AddLogin()
    .WithCredential(
        username: "login",
        password: "password")
    .AndRole(
        database: "acme")
    .AddRight(
        permission: "read")
    .AddRight(
        permission: "write");
  • Una nota en particular para los métodos de extensión en C#, una ventaja que tienen es que permite crear métodos que no existan en la interfaz lo que permite esconder algunos métodos que no queremos mostrar en la interfaz o darles una implementación concreta (sí, con C# 8 se puede hacer lo mismo con la implementación de código por defecto en interfaces pero en este caso no queremos ni mostrar el método en la interfaz), de hecho se podría crear Fluent Api solo con métodos de extensión indicando en Fluent Api simplemente la herencia, parecido a como sería con mixin.
    Aunque puede parecer más cómodo no resulta muy legible ni queda claro el propósito de cada interfaz, por otro lado al ser métodos de extensión solo se puede trabajar sobre objetos estáticos y eso puede complicar el código.
    El ejemplo propuesto tiene dos interfaces para dos propósitos, configurar conexiones y configurar usuarios, imaginemos que no queremos usar interfaces y solo usar una para configurar por ejemplo los usuarios y dejar la conexión en su configuración por defecto, este sería un ejemplo donde un método de extensión puede ser útil, de cara a la interfaz siguiendo con el patrón solo existe un método sin contexto que está en la interfaz principal IDatabaseBuilder.Save() pero vía extensión hay un método Save() para cada interfaz.
public static string Save(this IUsersBuilder usersBuilder)
{
    JsonSerializerSettings settings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All };

    return JsonConvert.SerializeObject((usersBuilder as DatabaseBuilder).Database, settings);
}

Reflexiones Fluent Api (II): Métodos

Reflexiones Fluent Api (III): Interfaces

Introducción

Aunque es posible implementar Fluent Api con clases realmente la limitación de algunos lenguajes con la herencia múltiple hace que la costumbre sea usar interfaces, de hecho resulta más útil ya que es posible crear diferentes implementaciones especialmente en sistemas de configuración.

Diagrama interfaces

Lo más relevante de cara a las interfaces es:

  • El nombre indica que contiene, esto quiere decir que no importa mucho que el nombre sea un poco largo si indica que métodos va a incluir o que se puede hacer con el, hay que tener en cuenta que el nombre de la interfaz generalmente no será visible en código.
    Tampoco es inusual que el nombre interfaz se componga como una concatenación de todos las interfaces padres lo que a la hora de buscar o entender el código resulta más cómodo a falta de un diagrama a mano.
  • Crear interfaces hija al añadir objetos, si el objeto interno tiene una lista y requiere añadir objetos a esa lista lo recomendable es crear una nueva interfaz para construir objetos de esa lista interna, y el método AddXX correspondiente devolverá dicha interfaz hija para que pueda ser usada para configurar ese elemento hijo, de esta forma se proporciona métodos específicos para este objeto que no pueden ser usados por otros en las interfaces superiores.
    En el siguiente ejemplo se agrega un usuario y se devuelve una ICredentialBuilder que es específica para crear las credenciales de un usuario.
public ICredentialBuilder AddUser()
{
    login = new Login();
    logins.Add(login);

    group.Logins.Add(login);
    return this;
}
  • Las interfaces hija siempre heredan directa o indirectamente de la principal, de lo contrario nos quedaríamos bloqueados y no podríamos acceder a los métodos de la interfaz principal para continuar creando el objeto y finalmente guardarlo a través del método sin contexto.
    En el siguiente ejemplo IUserBuilder hereda de IGroupBuilder para poder añadir usuarios a un grupo, es decir que directamente no hereda de la interfaz principal IUsersBuilder pero si a través de IGroupBuilder.
public interface IUsersBuilder
{
   ...
}

public interface IUserBuilder : IGroupBuilder
{
    ICredentialBuilder AddUser();
}

public interface IGroupBuilder : IUsersBuilder
{
    IUserBuilder AddGroup(
        string name = "");
}
  • Solo la interfaz principal debe tener un método void, como indica la propia definición de Fluent Api en algún momento debemos poder indicar que el objeto está terminado y se puede construir, esté método debería estar solo en la interfaz principal para evitar crear objetos incompletos, ejemplos clásicos son Build(), Save(),…
    Está bien, es verdad que se puede usar un método de extensión para sortear esta limitación, por ejemplo guardar resultados parciales, pero a no ser que sea necesario no es lo recomendable.
public interface IDatabaseBuilder : IConnectionBuilder, IUsersBuilder
{
    string Save();
}
  • Usar el mismo fichero para crearlas, si no son muchas es más cómodo tenerlas todas juntas en un fichero que en varios separados, sobre todo porque generalmente son interfaces con pocos métodos y al haber una relación jerárquica entre ellas vía herencia es más cómodo verlas todas juntas, como nombre de fichero lo más cómodo es poner el nombre de la interfaz principal que suele ser la que mejor define el uso.
namespace FluentApi
{
    public interface IUsersBuilder
    {
        ICredentialBuilder AddLogin();

        IGroupBuilder AddGroups();
    }

    public interface ICredentialBuilder : IUsersBuilder
    {
        IRoleBuilder WithCredential(
            string username = "",
            string password = ""
            );
    }
}
  • Así como usar métodos AddXXX indica que la interfaz que devuelve servirá para crear objetos de una lista interna, es posible tener mayores niveles de anidación, por ejemplo listas de listas, esto se puede solucionar creando una interfaz que represente esta relación de creación de listas, dentro de la implementación se hará la correcta creación de las mismas.
    En el ejemplo el método AddGroups() representa este efecto, este método devuelve otra interfaz IGroupBuilder encargada de crear la lista anidada y a su vez está interfaz tiene el método que añade elementos individuales AddGroup().
public interface IUsersBuilder
{
    ICredentialBuilder AddLogin();

    IGroupBuilder AddGroups();
}
public interface IGroupBuilder : IUsersBuilder
{
    IUserBuilder AddGroup(
        string name = "");
}
public interface IUserBuilder : IGroupBuilder
{
    ICredentialBuilder AddUser();
}
  • Puede ser que tengamos dos interfaces cada una de las cuales se encargue de una parte concreta de la creación del objeto, en este ejemplo hay una interfaz para crear conexiones IConnectionBuilder y otra para crear usuarios IUsersBuilder. Si necesitásemos usar ambas necesitamos una interfaz que nos las conecte, ese es el propósito de la interfaz IDatabaseBuilder.
    Mantenemos una referencia a la interfaz principal y la usamos para configurar cada una de las partes concretas del objeto.
IDatabaseBuilder databaseBuilder = DatabaseBuilder.Create();
databaseBuilder
    .AddLogin()
    .WithCredential(
        username: "login",
        password: "password")
    .AndRole(
        database: "acme")
    .AddRight(
        permission: "read")
    .AddRight(
        permission: "write");
databaseBuilder
    .WithServer(
        name: "server",
        port: 8000,
        version: new Version("0.0.0.1"));
var json = databaseBuilder.Save();
Reflexiones Fluent Api (III): Interfaces

Reflexiones Fluent Api (IV): Clases

Introducción

Detrás de una gran interfaz siempre hay una gran clase, y en este caso no es una excepción, en general si la interfaz está bien definida en términos de herencia y parámetros de devolución no debería haber problemas.

Diagrama clases

Algunas recomendaciones:

  • El objeto interno y en general todos los que se usen deben de ser privados, esto es obvio si permitiésemos que métodos externos modifiquen el objeto interno no tendría sentido usar Fluent Api, hay que revisar todos los modificadores de acceso a todos los objetos que se usen dentro de la clase.
  • Solo puede haber un objeto interno raíz, es cierto que puede interesar tener una colección pero en ese caso es mejor tener un objeto que albergue una colección, tener un solo elemento raíz simplifica el desarrollo, por lo que en caso de querer tener una colección sería mejor crear un objeto raíz que englobe esta colección.
public class DatabaseBuilder :
    IDatabaseBuilder,
    IUserBuilder,
    IRoleBuilder,
    IRightBuilder,
    IGroupBuilder,
    ICredentialBuilder
{
 ...
}
  • Los elementos no persisten hasta llamar al método sin contexto, cualquier uso de métodos modificará el objeto en memoría y no estará disponible a través de ningún método de acceso hasta que se llame al método sin contexto (Save(), Build()…) que es el que realmente creará el objeto o lo almacenará, si fuera acceso a una base de datos el commit iría en este punto, sino, es posible emular este efecto creando objetos o listas temporales.
    En el ejemplo de muestra se usan variables privadas que almacenan partes del objeto que se usan en varias partes del método.
internal readonly Database Database = null;
private List<Right> rights = null;
private List<Login> logins = null;
private List<Group> groups = null;
private Role role = null;
private Login login = null;
private Group group = null;
  • Crear varios ficheros, en este caso daría el consejo contrario a las interfaces, a no ser que sea poco código si hay varias interfaces es recomendable tener las implementaciones en ficheros diferentes para evitar mezclarlas y que se vea más claro el uso de cada clase.
  • Vigilar la inicialización y construcción de objetos, usar Fluent Api no debería ser complicado y una buena inicialización de objetos en las clases superiores permite que estén disponibles para métodos anidados y no tengan que preocuparse de una creación condicional.
    Si el código está correctamente relacionado llamar al método final sin contexto debería ser trivial.
    Por ejemplo el método AddGroup crea no solo la lista de posibles grupos anidados sino también la lista de logins ya que al llamar a este método la única opción es llamar a métodos de la interfaz IUserBuilder que va a usar estos objetos.
public IUserBuilder AddGroup(string name = "")
{
    groups = new List<Group>();
    group = new Group();
    group.Name = name;
    logins = new List<Login>();
    group.Logins = logins;

    groups.Add(group);
    Database.Groups.Add(groups);
    return this;
}
  • No usar constructores públicos, es mejor tener un método de factoría que devuelva la interfaz principal y sea esta la que se use durante la configuración, generalmente la creación del objeto por defecto suele ser necesaria y estos detalles deberían quedar ocultos.
    En el ejemplo se usar un método estático que crea el objeto interno y devuelve la interfaz principal para poder continuar configurándolo.
private static IDatabaseBuilder databaseBuilder = null;

public static IDatabaseBuilder Create()
{
    databaseBuilder = new DatabaseBuilder();
    return databaseBuilder;
}

private DatabaseBuilder()
{
    Database = new Database();
    Database.Users = new List<Login>();
}

Reflexiones Fluent Api (IV): Clases