miércoles, 24 de noviembre de 2010

Más sobre S.O.L.I.D.

En el último post os intenté convencer de que habíamos conseguido no violar el principio de inversión de dependencias con nuestra clase Banco. Si no lo recordáis, nos quedó algo como esto:



José Luis Barrera me sugirió en los comentarios que utilizáramos una clase Credenciales que agrupara tanto el nombre del usuario como el Pin. Con esa nueva abstracción, el código de Banco queda aún más claro:



Guay :D

Ahora bien, ¿qué ocurre cuando la validación de las Credenciales falla? Tal y como tenemos ahora mismo nuestra implementación, OperacionesBancarias lanzaría una excepción por cada tipo de error que se produzca al validar el usuario. Por ejemplo, si lo que falla es que las Credenciales son incorrectas se produce una excepción CredencialesIncorrectas. Si lo que sucede es que la validación de las Credenciales ha sido fallida más de tres veces, la excepción que se lanza es una AccesoInvalidadoPorMultiplesReintentosFallidos. Si lo que se produce es un error en el acceso al banco real, se lanzará un BancoInaccesible.
Veamos como implementaríamos un cliente de Banco (un Cajero) que se preocupe del resultado de la validación:



Pues no parece que sea muy legible... No se vosotros, pero yo estoy bastante acostumbrado a este tipo de código y tengo que decir ¡basta ya!
Las excepciones que captura Cajero no son parte de la abstracción de Banco, son detalles de bajo nivel que se encuentran en OperacionesBancariasBancoManolito, con lo que estamos volviendo a violar el principio de inversión de dependencias. Además, estamos controlando el flujo del programa mediante excepciones. ¡Que desastre!

Las excepciones deberían ser para casos excepcionales
Como bien nos contó (recordó) Enrique Comba en el curso de T.D.D., las excepciones deben ser excepcionales. No tiene sentido usarlas para controlar el flujo porque, en ese caso, dejan de ser excepcionales. Esto que parece tan trivial es una de las cosas que más nos cuesta cuando nos ponemos a programar.
Con esto en mente, volvamos a nuestro ejemplo, ¿Es excepcional que unas Credenciales sean incorrectas? ¿Es excepcional que un Banco tenga que anular una tarjeta porque el usuario se ha equivocado n veces al intentar usarla? De nuestro ejemplo, el único caso "excepcional" puede ser que el banco real no esté accesible pero ¿de verdad es tan raro que haya un corte de comunicaciones? Para mi, ninguno de estos casos es merecedor de una excepción, así que vamos a intentar arreglarlo. Lo primero que vamos a hacer es que Cajero no dependa de los detalles de OperacionesBancariasBancoManolito (y si de paso evitamos controlar el flujo con excepciones, mejor que mejor). Para ello, vamos a pasarle al Banco la responsabilidad de avisar al Cajero cuando suceda un evento de validación (correcta, incorrecta, invalida, sin conexión). Refatorizamos nuestro Banco para que quede como sigue:



y nuestro Cajero ahora queda mucho más limpio:



Aunque nuestra clase Cajero ha quedado mucho más clara, hemos pasado el problema de las excepciones a la clase Banco. Además, estamos violando el principio de segregación de interfaces (I). Vayamos paso a paso.

Segregación de interfaces

Clients should not be forced to depend upon interfaces that they do not use.

Los clientes no deben verse forzados a depender de interfaces que no usan.

Para cumplir este principio, nuestra clase Cajero debe implementar un interfaz para manejar los eventos de validación, que es lo único que la clase Banco necesita conocer de Cajero:



Y nuestra clase Banco dejaría de depender de Cajero para depender de dicho interfaz:



Ahora que nuestra clase Cajero ya cumple el principio de segregación de interfaces, arreglemos Banco.
La solución que se me ha ocurrido es que sea la abstracción Token la que nos de la información que actualmente nos dan las excepciones:



Ahora el flujo ya no es guiado por excepciones pero, lo que me parece más importante aún, es que nuestro código ahora es más sencillo de extender. Antes necesitábamos ir a un javadoc (o similar) para leer que tipo de excepciones eran necesarias para que Banco funcionara, ahora basta con implementar Token.

Por último, tengo que decir que no me gustan las estructuras if-elseif-elseif-else, pero para este caso en particular no me parece tan horrible. Si los motivos por los que Token puede no ser válido fueran más o pudieran cambiar más a menudo me pensaría una solución con suscriptores similar a la que hemos utilizado con el Cajero. Si os apetece hacerlo os lo dejo como ejercicio :P

Perdonad que me haya quedado una entrada tan larga y atolondrada. Espero que al menos se entienda lo que he intentado expresar :D ¡Nos vemos en la siguiente!

Nota: No haría estas refactorizaciones que he hecho aquí si no tuviera una buena base de pruebas contra las que probar cada pasito.

7 comentarios:

  1. Mmmm, me ha gustado este post, esto ya sí que parece OO.
    No puedo resistirme al pequeño ejercicio planteado, yo me quitaría ese if - else if tan largo, así, muy deprisa se me ocurre:
    Interfaz InteresadoAccesoACuenta hereda de InteresadoEnProblemas (o bien puedes tener dos interfaces diferentes y que Cajero implemente las dos, hay variantes). La segunda se especializa en problemas como credenciales inválidas, no se tiene permiso o banco no disponible. El método quedaría:
    public void obtenerCuenta(Credenciales credenciales) {
    Token token = operacionesBancarias.autenticarUsuario(credenciales);
    if (token.valido())
    suscriptor.cuentaObtenida(Cuenta.crear(token));
    else
    token.notificarProblema(suscriptor);
    }
    Así Token es una interface con varias implementaciones: TokenValido, TokenBancoNoDisponible, TokenPinInvalido, etc. El método notificarProblemas recibe como parámetro interfaz InteresadoEnProblemas.
    Se puede refinar aun más, usando comandos, con lo que desaparece Token, pero entonces el diseño ya cambia demasiado. También habría que ver que pinta Token en el método crear cuenta...
    Como siempre tenemos el eterno dilema del diseño: ¿Quién es más importante KISS o SOLID? ¿Se pueden satisfacer los dos a la vez? ¿En caso contrario, cómo los balanceo?¿Cuando dejo de refactorizar?
    Me ha gustado mucho leer este post ! :-)
    Salud !

    ResponderEliminar
  2. Me ha gustado la idea de tener varias implementaciones de token, aunque creo que si usamos token.notificarProblema(suscriptor) utilizando solo el interfaz InteresadoEnProblemas nos volvemos a cargar la segregación de interfaces (Un TokenPinInvalido solo debería entender de InteresadoEnPinInvalido, así que es bastante fácil de "arreglar").

    Respecto a lo de que pinta Token al crear Cuenta, la siguiente refactorización que estaba pensando hacer se carga Token y Banco :D No se si me dará tiempo a bloguearla y no se si tendrá que ver con SOLID o es más por vicio :P

    ResponderEliminar
  3. Muy interesante ejercicio, aunque no te lo compro para un "caso real".

    Mi sensación es que hace falta un equilibrio entre SOLID y YAGNI, o nos quedaremos atrapados en un ciclo de refactoring infinito :-). Y nuestro trabajo es entregar algo que ayude a otros, no que sea una "obra de arte" :-)

    Los cambios sugeridos en los comentarios hacen el código más elegante, pero también añaden más conceptos que el equipo de desarrollo tiene que conocer. Y eso solo para obtener la cuenta… en cuanto añadamos más…

    Por otra parte (y pensando en un problema real, no en un ejercicio), no me convence el patrón de usar efectos laterales (obtenerCuenta debería devolver una cuenta, como su nombre indica, no modificar otro objeto).

    Tampoco me acaba de gustar que un Banco tenga un único Cajero (o InteresadoEnAccesoACuenta).

    Está claro que el escenario es una simplificación para hablar de SOLID y que yo un quisquilloso, pero bueno... no he podido resistirme :-)

    ResponderEliminar
  4. Totalmente de acuerdo en balancear SOLID y YAGNI :) Hay que tener cuidado porque al final se te pira la pinza de mala manera.

    También te doy la razón en lo que dices de obtenerCuenta. Es un nombre muy malo, quizás un validarCredenciales o algo así evite malentendidos.

    Una pregunta, si no te gusta lo del suscriptor ¿Como manejarías los errores de validación? A mi lo de las excepciones del principio me gustaba menos que la solución con suscriptores, por eso lo cambié :P

    Respecto a lo de un banco con varios cajeros, en realidad, el ejercicio original era crear un software que controle un cajero físico, no un software que modele un banco. De todas formas, si quisiéramos poder manejar varios cajeros bastaría con tener un conjunto de suscriptores en lugar de uno solo.

    ResponderEliminar
  5. Muy bueno el código, muy ilustrativo.
    Creo ("creo") que Abel va no tanto al nombre ("validarCredenciales" no lo arregla) sino a que la comunicación al interesado se produce como efecto de una de las posibles ramas internas de obtenerCuenta, y no queda claro desde la lectura a alto nivel.

    Quizás ("quizás" ;) )... si el subscriptor se suministrara en ese método, y no en el constructor del Banco (que por otra parte sólo estaba ahí porque esto es código para jugar y aprender), el código... ¿quedaría más claro?

    Algo como:

    banco.abrirCuentaEnCajero(credenciales, cajero);

    al menos dejaríamos más claro que el cajero puede interactuar en este servicio...

    Sólo ideas rápidas (y así consigo no hacer lo que debería estar haciendo :) ), pero insisto, muy bueno el artículo!

    ResponderEliminar
  6. Pues tiene sentido :)

    Recuerdo que en el GOOS hacen algo parecido, a ver si me lo leo otra vez y me aclaro un poco más.

    Que difícil es todo, mare mía :P

    ResponderEliminar
  7. @Jorge: el cambio de nombre ayudaría algo… al menos sería "intuitivo" que devuelva "void". Las convenciones nos permiten leer el código… pero no es el tema del post :-)

    Estoy de acuerdo, Banco está hay para jugar. Pero a mi me bloquea… si la clase Banco no representa a un banco, tenemos un problema de comunicación. Yo la quitaría (moviendo código al Cajero) y buscaría otra abstracción.

    Sobre cómo trataría los errores… tiene pinta de que estás haciendo demasiadas cosas diferentes de una sola vez.

    1) Cuenta Anulada: esto es fácil que sea un atributo/subclase de la clase Cuenta
    2) Credenciales incorrectas: esto es algo que ocurre tras teclear el PIN y antes de acceder a ninguna de mis cuentas (método autenticar q devuelve null en lugar del usuario)
    3) Banco inaccesible: esto es algo que ocurre cuando el cajero se enciende. Desde mi punto de vista, que el banco deje de estar accesible (después de tener una conexión válida) es una excepción. A mi se me ha reiniciado un cajero en las narices (con mi tarjeta dentro) y lo considero Excepcional :-)

    Pero no estaba en el curso, no tengo claro el alcance del ejercicio o las restricciones que existían. Así que, bueno... no me hago responsable si incumples algún principio SOLID por mi culpa:-)

    ResponderEliminar