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