Patrón Step Builder

Como construir un builder más conciso y ordenado

Hola a todos ¡cuanto tiempo sin vernos! Estos últimos meses he estado con mucho trabajo y no he podido darle el cariño deseado a mi blog. Para no empezar muy fuerte les traigo un interesante artículo sobre un patrón de diseño de software no tan conocido pero que me ha resuelto una problemática que tenía en uno de mis últimos proyectos.

Posiblemente muchos de ustedes, sobretodo los que les gusta el Clean Code y el buen software artesano ya conocerán el patrón Builder que tan de moda se ha puesto en los últimos tiempos y que nos aporta una forma fluida y legible de construir objetos, normalmente con gran cantidad de parámetros opcionales en los que sería muy engorroso tener un constructor con muchos parámetros y muchas sobrecargas. Por ejemplo, la típica configuración de Spring Security para una aplicación web donde se nos inyecta un builder como parámetro para que construyamos la configuración deseada:


@Override
protected void configure(AuthenticationManagerBuilder auth) {

    auth.inMemoryAuthentication()
            .withUser("atrujillo").password("happy_decoding").roles("ADMIN")
            .and()
            .withUser("guest").password("hello_decoder").roles("USER");
}
    

Esa es la pinta de un Builder y podemos resumir sus ventajas en:

  • Flexibilidad a la hora de crear un objeto con diferentes configuraciones.
  • Legibilidad y semántica.
  • Separación de la creación de un objeto con su representación.
  • Fluent API

Aunque no todo es color de rosa y tal como os decía me encontré que en uno de mis últimos proyectos estábamos construyendo un SDK y utilizando mucho el patrón Builder para inicializar clases con gran cantidad de campos requeridos y opcionales. La problemática surgió al darnos cuenta de que el usuario del SDK no iba a saber en tiempo de compilación que parámetros eran obligatorios y cuales no, por lo que tenían que ir haciendo "ensayo y error" hasta dar con todos o en su caso estar viendo constantemente la documentación (cosa que no hacen gran parte de los programadores).

Para solventar esta problemática se me ocurrió mejorar nuestros Builder's para implementar el patrón Step Builder. Y ahora me preguntarás ¿que tienen de diferentes? Pues básicamente este patrón nos permite controlar el flujo del builder a la hora de construir los objetos, obligando a los usuarios de nuestras clases a instanciarlas de la manera que queramos y en un orden pre-definido.

Por ejemplo, supongamos que queremos programar algún tipo cliente para conectarnos a una red WIFI. Como sabemos, para conectarnos a una wifi hay parámetros imprescindibles que necesitamos configurar, sin embargo hay otros parámetros que son opcionales y solamente necesitaremos en algún caso específico. En esta problemática encaja muy bien nuestro patrón Step Builder. Entonces, hipotéticamente tendremos una clase que modela la conexión Wifi (llamémosla WifiConnection), esta clase tendrá un constructor privado y solo será posible crearla mediante un builder que nos obliga a introducir los campos requeridos, en este caso el nombre de la wifi(SSID) y el tipo de autenticación que usa. Veamos como quedaría:

Como véis, los primeros dos pasos del builder son obligatorios y con un orden predefinido (ssid y auth); después que se han completado estos dos pasos el builder nos dará la opción de crear ya el objeto o de seguir añadiendo otros campos opcionales (ipConfig).


Implementación

Para implementar este patrón hay que seguir esta serie de reglas a seguir:

  1. Crear los atributos de tu clase y tener claro cúales son opcionales y cuales son requeridos.
  2. Crear el constructor privado.
  3. Crear una "inner interface" por cada atributo requerido (llámemoslo paso). Esta interfaz debe devolver el siguiente paso.
  4. Crear el último paso, el cúal es especial, porque permite construir la instancia o añadir los atributos opcionales.
  5. Crear la clase estática builder como hija de nuestra clase. El builder debe implementar todos los pasos que hemos definido (recordad que son interfaces). En la implementación de los pasos es donde rellenaremos los atributos.
  6. Crear un método estático builder() que nos devuelva el primer paso.

Veamos un fragmento del código:


public class WifiConnection {

    //Regla 1
    private String ssid;
    private WifiAuth wifiAuth;
    private Proxy proxy;
    private IPConfig ipConfig;
    private WifiFrequency wifiFrequency;

    //Regla 2
    private WifiConnection(String ssid, WifiAuth wifiAuth, Proxy proxy, IPConfig ipConfig, WifiFrequency wifiFrequency) {
        this.ssid = ssid;
        this.wifiAuth = wifiAuth;
        this.proxy = proxy;
        this.ipConfig = ipConfig;
        this.wifiFrequency = wifiFrequency;
    }

    //Regla 3 aquí creamos los pasos
    public interface SsidStep {
        AuthStep ssid(String ssid);
    }

    public interface AuthStep {
        Build auth(WifiAuth auth);
    }

    //Regla 4 último paso
    public interface Build {

        WifiConnection build();

        Build proxy(Proxy proxy);

        Build ipConfig(IPConfig ipConfig);

        Build wifiFrequency(WifiFrequency frequency);

    }

    //Regla 6 Creamos el método de entrada que internamente está devolviendo una nueva instancia               del builder
    public static SsidStep builder() {
        return new Builder();
    }

    //Regla 5
    private static class Builder implements SsidStep, AuthStep, Build {

    ...
    ...

    }
}

 

Veamos en detalle la implementación del Builder:


private static class Builder implements SsidStep, AuthStep, Build {

    private String ssid;
    private WifiAuth wifiAuth;
    private Proxy proxy;
    private IPConfig ipConfig;
    private WifiFrequency wifiFrequency;

    @Override
    public WifiConnection build() {
        return new WifiConnection(this.ssid, this.wifiAuth, this.proxy, this.ipConfig, this.wifiFrequency);
    }

    @Override
    public AuthStep ssid(String ssid) {
        Objects.requireNonNull(ssid);
        this.ssid = ssid;
        return this;
    }

    @Override
    public Build auth(WifiAuth auth) {
        Objects.requireNonNull(auth);
        this.wifiAuth = auth;
        return this;
    }

    @Override
    public Build proxy(Proxy proxy) {
        this.proxy = proxy;
        return this;
    }

    @Override
    public Build ipConfig(IPConfig ipConfig) {
        this.ipConfig = ipConfig;
        return this;
    }

    @Override
    public Build wifiFrequency(WifiFrequency frequency) {
        this.wifiFrequency = frequency;
        return this;
    }
}

Como vemos en el builder simplemente vamos rellenando todos los atributos y devolviéndose a si mismo hasta que se llama el build().

La verdad que a pesar de ser un poco más trabajoso de construir que un builder tradicional a mi me ha gustado mucho conocer este patrón ya que le da más solidez y consistencia a nuestro software, evitando estados no esperados de los objetos y también los odiados NullPointerException. Como desventaja frente al método tradicional se podría decir que actualmente no existen plugins ni anotaciones como @Builder de Lombok que nos permitan generar automáticamente estos builders, pero bueno, habéis visto que tampoco es tan difícil ;) .

Si queréis ver todo él código didáctico he creado el siguiente repo: https://github.com/atrujillofalcon/step-builder-pattern-example

 

Feliz Semana Santa y Happy Decoding!!

 

Más entradas de blog

Spring Boot con Kotlin

En el mundillo del desarrollo web en Java está de más hablar de Spring Framework . A lo largo...

Como publicar nuestra librería Java con Gradle y Bintray

No hay nada más lindo que tu trabajo sea útil a otros. Como desarrolladores nos vemos inmersos...

Añadir comentarios