Después del interludio con el spring io, vuelvo a la carga con el tema del diseño ágil de software. Como ya comenté, considero que dentro de las limitaciones de tiempo y dinero de un proyecto, hay que estar continuamente reestructurando el código con el objetivo de que sea fácil y barato cambiarlo cada vez que se produzca un cambio funcional, añadamos un nuevo requisito o necesitemos arreglar un bug. A estas reestructuraciones de código se les llama «refactors».
Un refactor es una transformación de código que no altera la funcionalidad de este, pero sí su estructura. La idea es hacer refactors que hagan que nuestro código sea más flexible y maleable para poder alterarlo de forma rápida. El truco está en saber si al aplicar un refactor estamos mejorando nuestro código o si simplemente lo estamos complicando ¿Qué criterios sigo yo para evaluar si un refactor es bueno o no? Básicamente sólo sigo los tres siguientes:
- Que el código resultante sea más legible. Como ya expliqué considero este el criterio más importante.
- Que se respeten los principios DRY y «experto en información». Ya los explicaré en otro post
- Que se respeten los principios SOLID. Entre este post y los siguientes pienso ir explorándolos.
¿Qué es el SOLID? SOLID es un acrónimo que consolida 5 principios: Single responsability, Open/Closed, Liskov substitution, Interface segregation y Dependency inversion. En este post me ocuparé del principio de única responsabilidad (Single responsability o SRP), o sea la S de SOLID.
El principio de única responsabilidad (SRP) simplemente nos dice que ningún artefacto de código debe tener más de una única responsabilidad, y por lo tanto debe implementar una única funcionalidad. El principio aplica a sistemas, aplicaciones, frameworks, objetos y métodos. Obviamente según el artefacto al que se aplique, la amplitud del concepto de única responsabilidad cambia. Pero en este post nos vamos a centrar en cómo afecta a nuestra clases y objetos.
¿Cómo sabemos si un artefacto, como una clase o un método, respeta este principio? Es muy sencillo, simplemente debemos preguntarnos a nosotros mismos sobre cuantas razones diferentes podemos tener para querer cambiar dicho artefacto. Un artefacto que cumple el principio de única responsabilidad sólo puede ser cambiado debido a una única razón. Si encontramos múltiples razones para cambiar un artefacto entonces no cumple dicho principio y debe ser refactorizado. A mi me gusta llamar a este principio, el principio del bombero/torero. El hecho de que el personaje de bombero/torero nos cause risa es que es totalmente ridículo que una misma persona realice tareas de dos profesiones tan diferentes al mismo tiempo. Si lo vemos absurdo en una persona, ¿porque nos quedamos tan tranquilos cuando vemos una clase que es un bombero/torero?
Para entender mejor este principio veamos un ejemplo. Suponed que tenemos una historia de usuario tal que sigue:
«Cuando se realiza una transferencia importante
Entonces se genera un mensaje con información sobre dicha transferencia
Y se notifica a las personas de interés
Para que den su aprobación y así evitar el fraude fiscal»
Hablando con el cliente me dice que la persona de interés es un auditor, y que había pensado en notificarle vía mail. Las transferencias importantes son aquellas por encima de 1000 euros (son un poco ratas). Cómo estoy codificando en JAVA y usando el famoso framework Zprin, el código que me sale es el siguiente:
public class AuditorTransferenciasMonetarias { // Inyección de dependencias con el famoso framework Zprin (TM) // Con este framework somos productivos de la leche private ZprinPropertySource systemConfiguration; private ZprinTemplateEngine templateEngine; private ZprinMailIt mailIt; public void transferenciaRealizada(Transferencia transferencia) { if(this.esTransferenciaImportante(transferencia)) { String auditor=this.obtenerDireccionMailAuditor(); String mensaje=this.componerMensajeAviso(transferencia); ConexionMail conexionMail=null; try { conexionMail=this.abrirConexionMail(); conexionMail.enviar(new Mail().to(auditor).withBody(mensaje)); } finally { if(conexionMail!=null) conexionMail.cerrar(); } } } private boolean esTransferenciaImportante(Transferencia transferencia) { return transferencia.importe()>1000; } private String obtenerDireccionMailAuditor() { return this.systemConfiguration.getProperty("auditor.email"); } private String componerMensajeAviso(Transferencia transferencia) { return this.templateEngine.getTemplate("aviso-transferencia-importante.ztpl").execute(transferencia); } private MailConnection abrirConexionMail() { return this.mailIt.openConnectionTo(this.systemConfiguration.getProperty("servers.mail")); } }
Buen código, al usar el framework soy muy productivo y hago la clase super rápido, es más me quito de encima problemas complejos como el envío de mails, el acceso a propiedades configurables, o el tener que hacerme un motor de plantillas 😛 Sólo tiene un problema, que no cumple el SRP (maldito SOLID, quién lo habrá inventado). SRP nos dice que la clase sólo debe tener una responsabilidad y la única manera en la que esta debería tener que cambiarse es cuando hay alguna alteración en dicha responsabilidad. En este caso la responsabilidad viene definida por la historia de usuario, así que mientras esta no cambie de forma significativa, entonces no deberíamos vernos forzados a cambiar la implementación. Obviamente el código descrito arriba no cumple esta propiedad. Vayamos por partes.
Suponed que el criterio para clasificar a una transferencia como importante cambia. Nos veríamos forzados a tocar nuestra clase, aunque realmente nuestra historia de usuario es la misma. Para evitarlo alteramos nuestro código:
public class AuditorTransferenciasMonetarias { // .... public void transferenciaRealizada(Transferencia transferencia) { if(transferencia.esImportante()) { String auditor=this.obtenerDireccionMailAuditor(); String mensaje=this.componerMensajeAviso(transferencia); ConexionMail conexionMail=null; try { conexionMail=this.abrirConexionMail(); conexionMail.enviar(new Mail().to(auditor).withBody(mensaje)); } finally { if(conexionMail!=null) conexionMail.cerrar(); } } } // .... }
Como vemos ahora la clase Transferencia tiene un método que nos indica si es importante o no. Si el criterio de importancia cambia nos da igual, ya que este método nos protege de dicho cambio. Si el cliente decide que quiere un motor de reglas parametrizable en runtime mediante un backoffice, el afectado es Transferencia no la clase que estamos construyendo.
Otro motivo de cambio ajeno a nuestra responsabilidad es que la definición de «personas interesadas» cambie. De repente queremos notificar no sólo a un auditor, sino a varios y además al director de la sucursal. Para protegernos de ello veamos el siguiente cambio:
public class AuditorTransferenciasMonetarias { // .... // Ahora tenemos un colaborador de "negocio" // que está al mismo nivel de abstracción que esta clase private DirectorioEmpleados directorioEmpleados; public void transferenciaRealizada(Transferencia transferencia) { if(transferencia.esImportante()) { String mensaje=this.componerMensajeAviso(transferencia); ConexionMail conexionMail=null; try { conexionMail=this.abrirConexionMail(); conexionMail.enviar(rellenarDestinatarios(new Mail().withBody(mensaje))); } finally { if(conexionMail!=null) conexionMail.cerrar(); } } } private Mail rellenarDestinatarios(Mail mail) { for(Empleado interesado: this.interesadosEnTransferenciasImportantes()) mail=mail.to(interesado.email()); return mail; } private List<Empleado> interesadosEnTransferenciasImportantes() { return this.directorioEmpleados.empleadosConRol(Empleado.INTERESADOS_TRANSFERENCIAS_IMPORTANTES); } // .... }
Ahora gracias a la interface DirectorioEmpleados, podemos buscar a todos los empleados que necesitamos notificar. Para ello buscamos por rol. La asociación empleado/rol se puede definir aparte, e incluso ser dinámica y cambiar en runtime. Observad que hemos añadido un colaborador que está al mismo nivel de abstracción de la clase que estamos codificando, y que el código ahora se parece un poco más a la definición de la historia de usuario.
Otro motivo por el cual podemos vernos obligados a cambiar esta clase, es que ahora los mensajes no se envíen por mail, sino por SMS. O peor, ¡ que se envíen a la vez por SMS, Mail, y chat a la vez ! Como no queremos que nos pillen con eso (que me conozco al cliente, que cambia de opinión constantemente) volvemos a «mejorar» el código:
public class AuditorTransferenciasMonetarias { // ... private SistemaMensajeriaCorporativa mensajero; public void transferenciaRealizada(Transferencia transferencia) { if(transferencia.esImportante()) { this.mensajero .enviar( new Mensaje() .a(this.interesadosEnTransferenciasImportantes()) .conContenido(this.componerMensajeAviso(transferencia)) ); } } // .... }
Esta vez hemos introducido el sistema de mensajería corporativa, que nos permite enviar un mensaje de forma abstracta a varios empleados. Ya está, ya no hay más motivos de cambio… no perdón nos queda otro motivo y gordo. Hablando con nuestro amigo el «friki», nos cuenta que el framework Zprin ya no es el acabose y que ha salido el framework XprinGio, y que a Zprin se queda sin soporte en menos de dos años. Nos olemos la tostada, en cuanto se entere el arquitecto jefe del temita nos manda cambiar de framework, o peor, si se entera el jefe de proyecto que sólo nos quedan dos años de soporte, toma migración. Hay que protegerse a toda costa de tan temida migración:
public class AuditorTransferenciasMonetarias { private DirectorioEmpleados directorioEmpleados; private SistemaMensajeriaCorporativa mensajero; private PlantillasCorporativas almacenDePlantillas; public void transferenciaRealizada(Transferencia transferencia) { if(transferencia.esImportante()) { this.mensajero .enviar( new Mensaje() .a(this.interesadosEnTransferenciasImportantes() .conContenido(this.componerMensajeAviso(transferencia)) ); } } private Documento componerMensajeAviso(Transferencia transferencia) { return this.almacenDePlantillas.usandoPlantilla("avisos/transferencia-importante").crearDocumentoCon(transferencia); } private List<Empleado> interesadosEnTransferenciasImportantes() { return this.directorioEmpleados.empleadosConRol(Empleado.INTERESADOS_TRANSFERENCIAS_IMPORTANTES); } }
Esta vez hemos eliminado todo dependencia con Zprin, y tenemos un motor de plantillas abstracto. Obviamente cuando implementemos el motor de plantillas, o el sistema de mensajería nos acoplaremos a Zprin, pero en una posible migración, sólo tendremos que cambiar estos servicios y no todas las clases de negocio. Otro cambio es que ahora la plantilla genera un Documento en vez de un String. Además la clase Documento sabrá serializarse en cualquier formato que se necesite por parte del mecanismo de mensajería (String, XML, HTML, JSON, PDF…) lo que permite tener sistemas de mensajería más complejos.
Como se observa, si seguimos el principio de única responsabilidad nuestros artefactos tendrán una alta cohesión y por lo general un bajo acoplamiento, ya que quedan pocos métodos muy relacionados entre si y muy cortos. Pero lo mejor es que se tenderá a que la tasa de cambios de un único artefacto baje, y que éstos cambios sean más localizados, ya que sólo se va a necesitar modificarlo ante un único tipo de cambio al existir una relación uno a uno entre responsabilidad y clase. Obviamente este era un ejemplo de código algo artificial, y seguro que le sacais pegas, pero creo que podéis captar la idea.
Pero no es oro todo lo que reluce, más adelante en esta serie veremos el principio DRY y el «experto en información» y como ambos se terminan pegando con el principio de única responsabilidad, con lo que se produce un conflicto grave. Para colmo, el principio de segregación de interfaces nos puede complicar también la vida. Armonizar todos estos principios, es el caballo de batalla del diseño orientado a objetos. De hecho, respetar cada principio por separado es sencillo, lo difícil es juntarlos todos, ya que veréis que es como si unos principios contradijeran a otros ¿Terminará siendo la OO en realidad un paradigma fracasado y contradictorio?
Apenas me he leído el enunciado del problema y he visto la clase final AuditorTransferenciasMonetarias y en mi opinión… ¡¡CHAPÓ!!
No se cuales serán tus autocríticas futuras pero recuerda que el riesgo de sobreingeniería existe y lo más importante no es tratar de cumplir principios de forma purista perdiendo por el camino el objetivo que buscas.
«Esta vez hemos eliminado todo dependencia con Zprin»
Genial pero la búsqueda de la independencia absoluta de las librerías que utilizas puede llegar a ser absurda, la independencia absoluta es una quimera, en algún lugar existe esa dependencia, el excesivo énfasis en la independencia puede llevar al absurdo de hacer una API que sea prácticamente una capa idéntica pero con nombres cambiados de la API que estamos «independizando».
Hay dos razones en mi opinión mucho más importantes que la búsqueda de la independencia del framework Zprin (que por cierto es una librería muy buena):
1) Romperías el principio de una responsabilidad al meter la composición del mensaje en AuditorTransferenciasMonetarias, cuando dicha clase es «orquestadora» del requisito/funcionalidad/historia de usuario/regla de negocio (como se quiera llamar), pero no debe meterse en los detalles de como se construye el mensaje, y el uso directo del framework Zprin invita a entrar en esos detalles porque su API es de «bajo» nivel
2) Das un significado semántico al proceso de enviar el mensaje (SistemaMensajeriaCorporativa), es decir acabas construyendo una clase que es la que sabe «como» enviar un *mensaje corporativo* usando una determinada *plantilla corporativa* a una lista de *empleados* con información sobre una *transferencia*. El valor semántico y de comprensión del programa que da esta clase, en mi opinión es mucho más importante que el problema de tener que cambiar Zprin por XprinGio (por cierto muy bueno también), pues dichos cambios los tendrás que hacer de todas formas en la clase SistemaMensajeriaCorporativa
Cierto, las dos razones que esgrimes son más importantes. Sobre todo la segunda que es la que realmente da mayor legibilidad al código. Como ya comenté en un post anterior considero la legibilidad como de lo más crítico para la «calidad» del software.
Respecto a como evitar la sobreingeniería, es sencillo: refactoriza respetando las restricciones de tiempo y dinero que tienes para entregar valor al cliente.
Cuando digo «Romperías el principio de una responsabilidad» no lo digo en plan «no puedo romperlo porque Fulanito dice que tiene que ser así», es puro sentido común que invita a no hacer demasiadas cosas en una clase y menos aún si apenas están relacionadas.
Lo de que sea «una» y no «dos» me trae al pairo, pues siempre es posible autoengañarse y pensar que «el envío del mensaje forma parte de las responsabilidad de auditar la transferencia» y ya está ya cumples el ppio de única responsabilidad, más bien lo importante es que el «como» se envía el mensaje es lo suficientemente concreto para segregarlo a una clase diferente.
«¿Terminará siendo la OO en realidad un paradigma fracasado y contradictorio?»
Después de cerca de 30 años de uso real yo no lo veo muy fracasado, más bien su problema es que a la hora de la verdad se usa MUY POCO, unos por desinterés, otros por desconocimiento y otros llevados por falacias tipo la herencia supone un excesivo acoplamiento y cosas así.
Sobre si pienso si el paradigma OO ha fracasado pienso ahondar en futuros post. La cosa no es trivial. Incluso algunos autores hablan de dos paradigmas distintos que han sido englobados por la «sociedad» dentro del mismo saco de la OO: la orientación a clases y la orientación a objetos (pura y verdadera 😉 )
Intuyo por donde vas, ciertamente la orientación a clases no es orientación a objetos, lo único que «consuela» al menos es que exista «orientación a objetos light» que es cuando se usan interfaces y al menos dos clases implementan una interface, pobre pero algo es algo.
En mi opinión hemos pasado del abuso al desierto, pero de eso ya hablaremos en tu proximo artículo.
Hola enrique, buenísimo el post, me gusta mucho el estilo «fluent» pero le quitaría esos «this» jeje. Realmente es difícil balancear todos los principios y en ocasiones muchos parecen contradictorios o es casi imposibles cumplirlos todos, en muchas ocasiones es cuestión de llegara un compromiso razonable y utilizar los principios como avisos o indicaciones de hacía donde podríamos dirigir nuestro diseño.
Lo que a mi mas ha echo darle vueltas a la cabeza es lo que mencionas al final, cuando dices que el patron experto se pega con el SRP (y con el IS tb). Por ejemplo, según el patrón experto donde tendría sentido meter la funcionalidad de enviar el mensaje a los interesados en la transferencias importantes es en la propia transferencia, pero claro si hacemos esto nos cargamos el SRP porque confirmar una transferencia y enviar mensajes parecen desde luego dos responsabilidades claramente diferenciadas. Además dentro de la transferencia tendríamos que manejar el estado (confirmada o no etc).
La cosa es que a lo mejor lo que podemos cambiar es lo que parece más obvio que esta bien, quizas lo que no exista es una clase «transferencia». Por ejemplo podríamos tener una clase «TransferenciaPendiente» y otra clase «TransferenciaConfirmada» de modo que la pendiente confirma y la relizada envía mensajes. una cosa asi:
TransferenciaPendiente tp = nuevaTransferencia(….);
TransferenciaConfirmada tr = tp.confirmar();
tr.enviarMensajeALosInteresados();
De este modo las responsabilidades están separadas y cumplimos el patrón experto, de paso también cumplimos con el interface segregation.
El problema de no cumplir el patron experto es que en algún momento (para enviar los datos del mensaje) necesitaremos acceder a las interioridades de la transferencia desde fuera, es decir, preguntamos cosas a un objeto en lugar de pedirle que haga cosas, en definitiva, no hacemos «tell don’t ask».
Ultimamente lo que más me ronda la cabeza es que tendemos a ver «el modelo» de la aplicación y más que eso lo que tenemos son «los modelos», es decir, en cualquier aplicación no trivial existe un modelo compuesto por varios submodelos y con esta idea podemos intentar cumplir tanto el patrón experto como el SRP y hacer «tell don`t ask» que realmente es algo que yo nunca he conseguido del todo.
No se que te parece el enfoque enrique, si crees que he dicho muchas tonterías no me lo tengas en cuenta que estoy en casa con un trancazo importante y lo mismo es cosa de la fiebre 😛
Je, je, veo que has sufrido en tus carnes el conflicto entre SRP y «experto en información».
El problema de tener una clase diferente por estado que puede tener la transferencia, es que si llamas a confirmar, después te tienes que acordar de llamar a enviarMensajeALosInteresados, para no dejar transferencias confirmadas sin notificar.
Si hacemos que sea el método confirmar() quien internamente mandar la notificación, entonces nos la pegamos también con el Open/Closed, ¿cómo añadimos o alteramos reglas de notificación sin romper el «experto» en información? ¿Tal vez deberíamos crear un comando ConfirmarTransferencia?
En siguientes post pienso ir explorando los principios SOLID, DRY y «experto», para que todo el mundo los conozca, y después meterme más de lleno en como intentar solucionar estas paradojas y conflictos. Pero os dejo un mini adelanto: el problema es la orientación a clase. No se puede resolver con lenguajes orientados a clase como Java o C#.
Salud !
P.S. A ver si te recuperas de la fiebre y te vemos el Jueves.
Brillante en la exposicion y en los conocimientos expuestos. Mi unica pega es que no acabo de acostumbrarme a nombres tan largos. Al que solo lee una vez el codigo tal vez le ayude a entenderlo, aunque a mi casi me aturulla ya asi de primeras. Para el que tiene que leerlo una y otra vez (y escribi..ctrl+scp..lo!!) puede que incluso llegue a hacerle menos agil su mantenimiento.
Si hay documentacion y/o tests, convertir el codigo en documentacion no es violar el DRY???
Bueno, en inglés queda más corto, la verdad es que el español para programar es un poco «verbose» 😀
Yo prefiero poner nombres descriptivos en clases, métodos y variables. El «javadoc» se genera automáticamente asi que no podemos considerarlo una violación del DRY. En la documentación que no se genera automáticamente prefiero centrarme más en ejemplos de uso y tutoriales. Para la API, el javadoc y los tests.
En cualquier caso, yo creo que esto de los nombres es más cuestión de gustos, y un poco depende de las convenciones del lenguaje que uses. En JAVA con un IDE y CTRL+SPACE no es problema. En JS puede ser un poco «partededos» poner nombres largos.
Salud !
jummm o sea que si genero codigo repetido de forma automatica con el eclipse o si lo pego con ctrl v no se considera codigo repetido??? 😛
Es broma, yo incluso en java me parece que tiene su precio el usar nombres tan largos: o depender del ide a la hora de escribir o picar mucho y poder equivocarte mucho mas en alguna letra y a la hora de tener que leer es mucho mas fatigoso ya que una vez que sabes queUnMetodoHaceXParaCOnseguirY pues tienes que eliminar mentalmente todas esas letras que ya te sobran y recorrerlas con los ojos y con el scroll.
Se puede aprovechar el espacio de nombres de los paquetes para llegar a un compromiso:
my.company.transferenciasMonetarias.Auditor o lo que sea que tenga sentido en este caso
[…] – @eamodeorubio nos da una pequeña master class sobre diseño ágil, en concreto en este post (que promete no ser el último) se habla sobre el principio de única responsabilidad (SRP) y las clases bombero / torero. […]