Recursos programación

lunes, 23 de abril de 2018

Concurrencia - Clean Code


Esta publicación se centra en el capítulo de Concurrencia de Clean Code (Robert C. Martin).

Por qué concurrencia


Las aplicaciones que se ejecutan en un sólo hilo son relativamente fáciles de entender ya que el qué se hace y el cuando se hace están fuertemente acoplados. En cualquier momento se puede añadir un punto de ruptura en la ejecución para averiguar el estado del sistema.

Hay casos en los que el sistema se verá en la obligación de hacer varias tareas simultáneamente. Por ejemplo, si tomamos como ejemplo la creación de un agregador de noticias con una frecuencia de actualización de un minuto que se nutra de distintos RSS seleccionados, a medida que la cantidad de fuentes ascienda, la agregación de noticias podría aumentar la frecuencia de adición haciendo que sea superior a un minuto.En este caso, se podría hacer una consulta simultánea de las fuentes para mejorar el rendimiento considerablemente.

Desacoplar el qué hay que hacer del cuándo puede mejorar tanto el rendimiento como la estructura de la aplicación. Desde el punto de vista estructural, la aplicación es como un conjunto de pequeños sistemas que colaboran entre sí más que un sólo sistema el cual tiene un sólo hilo de ejecución.

Mitos y confusiones


La concurrencia implica cierta complejidad y si no se tiene en cuenta puede derivar en situaciones inesperadas.

Los principales mitos y confusiones:

  • La concurrencia siempre mejora el rendimiento. Esto dependerá de si el sistema tiene un tiempo de espera por hacer ciertas tareas de maneras secuencial. Si estas pueden ser realizadasde manera paralela, en este caso habrá una mejora del rendimiento.
  • El diseño no cambia cuando se desarrollan programas concurrentes. Desacoplar el qué del cuándo deriva, inevitablemente, en diseños distintos.
  • Entender los problemas de concurrencia no es importante cuando se trabaja con contenedores como una web o un contenedor EJB. Es importante saber qué está sucediendo en el contenedor y cómo abordar los problemas que puedan surgir.

Aspectos de la concurrencia:

  • La concurrencia implica cierta sobrecarga ya que hay que escribir código adicional.
  • La concurrencia es compleja incluso para problemas simples.
  • Los problemas de concurrencia no son fácilmente reproducibles por lo que se suelen ignorar o tratar como problemas puntuales.
  • A veces la concurrencia necesita un cambio fundamental en la estrategia de diseño.

Retos


La concurrencia plantea nuevos retos hasta en sistemas triviales. Observemos un sistema sencillo con la siguiente clase:

Si el sistema crea una instancia de esta clase, le asigna el valor 10 y esta instancia se comparte en dos hilos de ejecución diferentes. Dependiendo de cómo se hagan las llamadas al método «getNextId» podrían darse los siguientes casos:

  • El hilo de ejecución número 1 obtiene el valor 11 y el hilo de ejecución número 2 obtiene el valor 12. El valor del atributo «lastIdUsed» es 12.
  • El hilo de ejecución número 2 obtiene el valor 11 y el hilo de ejecución número 1 obtiene el valor 11. El valor del atributo «lastIdUsed» es 12.
  • El hilo de ejecución número 1 obtiene el valor 12 y el hilo de ejecución número 2 obtiene el valor 12. El valor del atributo «lastIdUsed» es 12.

En el primer caso el hilo de ejecución número uno se ejecuta antes que el dos mientras que en el segundo caso es al revés. El tercer caso es especial, ya que los hilos se ejecutan simultáneamente y estos adoptan el valor inicial de manera que los dos hilos de ejecución tienen un valor de 10, el cual incrementan y devuelven.

Principios de defensa con la concurrencia

Principio de responsabilidad única o Single Responsibility Principle


El principio de responsabilidad única, o SRP por su siglas en inglés, establece que un método, clase o componente debería tener una única razón para cambiar. El diseño concurrente es lo suficientemente complejo, por sí mismo, como para ser un razón para cambiar y, por lo tanto, merece ser separado del resto del código. A pesar de esto, la implementación de la concurrencia está incluida directamente en el código de producción. Aspectos a tener en cuenta:

  • El código relativo a la concurrencia tiene sus propio ciclos de desarrollo, cambio y personalización.
  • El código relativo a la concurrencia tiene sus propios retos, distintos de los que plantea el código no concurrente.
  • El número de formas en las que puede fallar el código basado en una concurrencia mal escrita es suficiente reto como para aislarlo del resto del código.

La recomendación es separar el código relativa a la concurrencia del no concurrente.

Corolario: límite del alcance de datos


Como se vio anteriormente, dos hilos de ejecución pueden compartir un objeto y modificarlo de manera que se obtenga un comportamiento inesperado. Una solución consiste en usar alguna característica del lenguaje para la sincronización y proteger las secciones críticas que son compartidas por los hilos de ejecución. Es importante reducir el número de estas secciones ya que:
  • Se puede olvidar proteger esta sección.
  • Habrá una duplicación de esfuerzo para que todo funcione correctamente (esto es una violación del DRY).
  • Será difícil modificar la fuente de los fallos ya que son complicados de encontrar.

La recomendación es llevar la encapsulación al extremo y limitar el acceso de cualquier dato que se comparta.

Corolario: usar copias de datos


Una estrategia para evitar compartir datos es no compartir datos. En algunas situaciones será posible copiar los objetos y tratarlos como un copia de sólo lectura. En otros casos será posible recopilar los objetos de los distintos hilos de ejecución y mezclarlos en un hilo determinado.

Si existe una forma sencilla de evitar los objetos compartidos el código será menos complejo. La creación de objetos adicionales es una ventaja en comparación con la dificultad del código compartido.

Corolario: los hilos de ejecución deberían ser tan independientes como sea posible


Se recomienda crear el código de forma que los procesos no compartan datos para evitar problemas de sincronización.

Conocer la librería


Dependiendo del lenguaje de programación este tendrá sus propias características que el programador deberá considerar.

Conocer los modelos de ejecución


Hay diversas maneras de separar el comportamiento en una aplicación concurrente. Hay algunos conceptos previos para poder entender cómo separar el comportamiento:

  • Recursos vinculados: recursos que tienen un tamaño o número fijo como puede ser una conexión a la base de datos.
  • Exclusión mutua: sólo un proceso puede acceder a la vez a un recurso o datos compartidos.
  • Inanicción: se impide que los procesos consuman demasiado tiempo o se ejecuten indefinidamente dejando relegados a otros que pueden no llegar a terminar de ejecutarse nunca.
  • Bloqueo: dos o más procesos esperan a que el otro termine. Se puede dar el caso de que dos procesos tengan cada uno un recurso compartido que el otro está esperando.
  • Bloqueo activo: procesos que tratan de avanzar pero se estorban unos a otros.

Productor-consumidor


En este modelo de ejecución uno o más hilos de ejecución (los productores) crean algún trabajo y lo añaden a un buffer o cola mientras que uno o más hilos de ejecución (consumidores) obtienen ese trabajo del buffer o cola.

El buffer o la cola es el recurso compartido. Los productores deben esperar a que se libere espacio para seguir produciendo mientras que los consumidores deben esperar a que haya algo para consumir.

Lectores-escritores


Como en el modelo anterior hay un recurso compartido en el cual los escritores actualizan información mientras que los lectores utilizan esa información. Si los escritores actualizan la información regularmente pueden llegar a impactar en el rendimiento de los lectores y viceversa.

El desafío consiste en equilibrar las necesidades de los escritores y lectores.

La cena de los filósofos


Imaginando una cena de un grupo de filósofos en una mesa redonda en la cual cada filósofo tiene un tenedor a su izquierda. Hay un gran cuenco de comida en el centro. Los filósofos sólo sólo comen si tienen hambre y no piensan. Cuando uno de ellos tiene hambre tiene que coger los dos tenedores (el suyo y el de su compañero) y comer del cuenco. Si el filósofo de su izquierda o derecha ya está usando uno de los tenedores tendrá que esperar a que termine de comer y lo deje en su sitio.

Reemplazando los filósofos por hilos de ejecución y los tenedores por recursos compartidos se tiene un problema de concurrencia.

Cuidado con las dependencias entre métodos sincronizados


La recomendación es evitar usar más de un método en un objeto compartido ya que se pueden producir bugs sutiles.

Cuando no se puede evitar hay unas reglas:

  • Bloque basado en cliente: el cliente bloquea el recurso antes de invocar el método y asegura que este método incluye el código que invoque al último método.
  • Bloqueo basado en servidores: se crea un método en el servidor que bloquee el propio servidor e invoque los métodos para más tarde anular el bloqueo. El cliente debe invocar este nuevo método.
  • Servidor adaptado: se crea un intermediario que es el que realiza el bloqueo y los métodos.

Reducir al mínimo las secciones sincronizadas


Las secciones de código que están protegidas añaden un bloqueo que genera retrasos y sobrecarga en el sistema, por ello este ćodigo debe ser el estrictamente necesario para proteger la sección crítica.

Escribir un código de cierre correcto es complicado


Los problemas de concurrencia como los bloqueos son habituales. La recomendación en este caso es repasar los algoritmos y dedicarles tiempo ya que no es algo trivial. Hay que pensar cómo hacer un cierre con antelación y probarlo hasta que funcione.

Probar el código con procesos


Probar el código que es correcto es muy complicado ya que las pruebas no garantizan que esté funcionando correctamente aunque puede minimizar el riesgo.

La recomendación es crear test que expongan los problemas potenciales y ejecutarlos frecuentemente con diferentes configuraciones y cargas. Si se produce un fallo hay que inspeccionarlo y no se debe ignorar porque en pruebas posteriores no se produzca.

Recomendaciones más concretas:

  • Considerar fallos como posibles problemas de los hilos de ejecución.
  • Hacer que funcione primero el código sin distintos hilos de ejecución.
  • Hacer que el código interaccione con otros hilos de ejecución.
  • Hacer el código modificable.
  • Ejecutar el código con más hilos de ejecución que procesadores.
  • Ejecutar en diferentes plataformas.
  • Diseñar el código para probar y forzar los fallos.

Considerar los fallos como posibles problemas de los hilos de ejecución


El código que se ejecuta en distintos hilos puede fallar aún en los casos más simples. Replicarlo puede llegar a ser una tarea frustrante por lo que, a veces, se ignoran estos errores. Cuantos más errores de este tipo se ignoren más desarrollo se hace sobre este código defectuoso.

La recomendación es no ignorar los fallos del sistema como algo puntual.

Hacer que funcione primero el código sin distintos hilos de ejecución


La recomendación es que el código funcione en un hilo de ejecución y asegurar que funciona correctamente.

Hacer que el código interaccione con otros hilos de ejecución


En este momento el código que ya funciona en un único hilo pasa a interaccionar con otros hilos de ejecución. El código tiene que estar desarrollado de manera que se pueda ejecutar:

  • Un proceso, varios procesos y variar el número de estos según se está ejecutando.
  • El código puede interactuar con algo que puede ser verdad o pruebas dobles.
  • Ejecutar el código con pruebas dobles de forma rápida, lenta y variable.
  • Configurar pruebas que ejecutar y ejecutarlas en distintas interacciones.

La recomendación es que el código debe poder interactuar con otros elementos y ejecutarse en distintas configuraciones.

Hacer el código modificable


La obtención del equilibro adecuado del sistema para los procesos suele requerir de prueba y error. En las fases iniciales hay que medir el rendimiento del sistema con distintas configuraciones. Una configuración fundamental es permitir un número de hilos y que el número de estos pueda variar mientras el sistema se está ejecutando. También se debería permitir la modificación automática en función de la producción y la utilización del sistema.

Ejecutar el código con más hilos de ejecución que procesadores


Si hay más hilos de ejecución que procesadores entonces habrá más frecuencia de intercambio entre los hilos de ejecución lo que aumentará las posibilidades de que surjan problemas.

Ejecutar en diferentes plataformas


Si el código va a ejecutarse en distintas plataformas es fundamental probarlas en todas ya que cada sistema operativo tiene sus propias políticas de ejecución.

La recomendación es probar en todas las plataformas y desde el principio del desarrollo.

Diseñar el código para probar y forzar los fallos.


Como la detección de los errores puede ser complicada y su replicación aún más, se puede diseñar el software de manera que se pueda forzar a que se ejecute en distinto orden añadiendo algunos métodos como pueden ser «wait», «sleep», «yield» y «priority».

Cada uno de estos métodos altera el orden de ejecución pudiendo aumentar así las posibilidades de que se produzca un error. Hay dos estrategias para forzar los fallos:

  • Manual
  • Automática

Manual


En esta estrategia se utilizan los métodos manualmente con el fin de probar un fragmento de código más problemático. Esto se hace mediante pruebas y no deben llegar a producción.

Automática


Existen herramientas como la estructura orientada a aspectos para tal fin. Estas herramientas se encargan de utilizar estos métodos en ejecuciones aleatorias aumentando considerablemente la capacidad de detectar los errores.

Conclusiones


El código concurrente no es fácil de desarrollar ya que hasta el código más sencillo se puede complicar.

Se debe diseñar el código pensando en el principio de responsabilidad única, empezando por la ejecución en un único hilo para más tarde interaccionar entre los recursos compartidos.

Conocer los problemas de concurrencia siempre es de ayuda para enfrentarse a otros más comunes.

Conocer las herramientas que proporciona el lenguaje de programación o la biblioteca es fundamental para poder resolver los problemas.

Es necesario reducir al mínimo el código que se puede bloquear, evitar invocar una sección bloqueada desde otra y reducir la cantidad de objetos compartidos.

Inicialmente será complicado detectar errores hasta que se hagan pruebas de carga más exhaustivas. Por esta razón el código se debe poder ejecutar con distintas configuraciones y plataformas.

La probabilidad de detectar errores mejora si se diseña el código y se hacen pruebas forzando los fallos ya sea de manera manual o automática.

viernes, 20 de abril de 2018

Diseño emergente - Clean code

Esta publicación se centra en el capítulo de Diseño emergente de Clean Code (Robert C. Martin).

Kent Beck establece cuatro reglas para crear un Diseño simple (Simple Design) que son fundamentales a la hora de crear un software bien diseñado. Un diseño es simple sigue las siguiente reglas:

  • Pasa todos los test.
  • No contiene código duplicado.
  • Expresa la intención del programador.
  • Minimiza el número de clases y métodos.

Diseño Simple Regla 1: Pasa todo los test


Un sistema debe tener el comportamiento para el cual fue diseñado. Este debe poder ser verificable para poder asegurar que se está comportando de manera adecuada. Para saber si un sistema es verificable, debería ser probado. Los sistemas que no se pueden probar no deberían ser desplegados ya que no hay garantías en lo referente a su su comportamiento. Un sistema que ha sido probado exhaustivamente y pasa todos los test es un sistema verificable.

Lo sistemas verificables permiten un diseño donde las clases son pequeñas y tienen un único propósito. Esto facilita que las clases cumplan el Single Responsibility Principle (SRP). Cuanto más pruebas se escriban más módulos se irán añadiendo que sean más fáciles de probar. Así que si un sistema está plenamente probado ayuda a crear mejores diseños.

Un mayor acoplamiento hace más difícil escribir test por lo que cuantos más test se escriben más sencillo será usar el principio de Dependency Inverse Principle (DIP) y otras herramientas como la inyección de dependencias, las interfaces y la abstracción para minimizar el acoplamiento.

Diseño simple Regla 2: no duplicar


La duplicación es el enemigo principal de un sistema bien diseñado. Este representa trabajo y riesgo adicional, además de añadir complejidad.

La duplicación se puede manifestar de diversas formas ya sea a través de un código que sea idéntico, muy parecido o incluso que tenga una implementación duplicada.

Por ejemplo, si hay dos métodos en una colección de objetos:


El método «isEmpty» podría devolver un valor binario mientras que «size» devolvería un número entero. La duplicación se puede evitar haciendo que «isEmpty» esté relacionada con la definición de «size».

Crear un sistema limpio requiere eliminar la duplicación incluso si son pocas líneas de código. Observemos el siguiente ejemplo.

Para mantener el sistema limpio hay que eliminar la pequeña duplicación.

Al extraer funcionalidad en estos pequeños niveles se empiezan a reconocer las vulneraciones del Single Resposibility Principle (SRP). Así que este nuevo método extraído se podría mover a otra clase de manera que se eleve su visibilidad. También se puede abstraer el método y se puede reusar en un contexto diferente. Este tipo de reutilización consiste en hacer que la complejidad de un sistem se reduzca sustancialmente. Entender cómo lograr la reutilización del código en pequeño es en esencia reutilizar el código en partes más grandes.

El Template Method pattern es una técnica para eliminar la duplicación a un nivel superior. Observemos el siguiente ejemplo.


Los dos métodos tienen el mismo código a excepción del cálculo de la puntuación por la imagen o el vídeo. El algoritmo cambia dependiendo del tipo de post que sea.

Se puede eliminar la duplicación aplicando el Template method pattern.

Las subclases sobreescriben el método de manera que implementan la lógica específica para cada tipo de post.

Expresividad


Cuando un programador escribe un código lo hace en base a su comprensión del problema que está resolviendo. Una vez pasado un tiempo y si ese mismo programador tiene que volver a tocar esa parte de código es muy posible que no entienda su propio código hasta que no vuelva a adquirir ese conocimiento más profundo del problema.

La mayor parte del coste de un proyecto de software a la larga es el del mantenimiento. Cuanto más grande es el sistema más complejo será entender qué hace cada una de sus partes. Por lo tanto el código debería comunicar la intención tan claramente como sea posible para que otros tengan que invertir menos tiempo en su comprensión. Esto reduce las equivocaciones y el coste de mantenimiento.

Los siguientes puntos son fundamentales:

  • Elegir buenos nombres que comuniquen la responsabilidad de cada clase y método.
  • Que estos sean lo más pequeños posibles.
  • Usar nomenclatura estándar del ámbito de la programación como patrones de diseño que describen la intención del diseño a otros programadores.
  • Baterías de pruebas unitarias que estén bien escritas y expresen lo que se está probando.
  • Aunque lo más importante es intentarlo. Ya que en demasiadas ocasiones una vez que el problema está resuelto se pasa al siguiente problema sin mejorar el código para la siguiente persona que lo tenga que leer.

Clases y método mínimos


En un esfuerzo por cumplir las reglas anteriores se puede llegar a crear clases y métodos que no son necesarios.

Se podría dar el caso de que hubiera un estándar de codificación que establezca crear una interface por cada clase. En este caso habría que ser más pragmático y no tener tantas clases e interfaces.

La meta final es mantener el sistema con las clases y métodos mínimos. Esta regla es la que menos prioridad tiene de las cuatro.

Conclusión


No hay unas simples prácticas que puedan reemplazar la experiencia, pero aplicando estas reglas se puede incrementar el aprendizaje que de otra manera se hubiera tardado más tiempo en adquirir.

miércoles, 18 de abril de 2018

Sistemas - Clean code

Esta publicación se centra en el capítulo de Sistemas de Clean Code (Robert C. Martin).

Separar la construcción de un sistema de su uso


Según Robert C. Martin:

«Software systems should separate the startup process, when the application objects are
constructed and the dependencies are “wired” together, from the runtime logic that takes
over after startup.»

«Los sistemas de software deberían separar el proceso de arranque, cuando se construyen los objetos de las aplicaciones y se “enlacen/conecten” sus dependencias, de la lógica en tiempo de ejecución que toma el control tras el inicio.»

El proceso de arranque es uno de los aspectos que cualquier aplicación debe abordar. La separación de aspectos es una de las técnicas de diseño más importantes.

El siguiente código podría formar parte del proceso de arranque de un sistema. Es este se puede apreciar que el código que construye el objeto de la base de datos está mezclado con la lógica del tiempo de ejecución qué es el que devuelve el objeto. Por ejemplo:


Esta técnica se llama Lazy Initialization/evaluation. Con esta técnica no se sobrecarga la construcción ya que hasta que no se use el objeto no se crea por lo que el sistema acelera su proceso de arranque. También evita que devuelva un valor nulo.

Un inconveniente es que la función tiene una dependencia de «DbConnectionImp» en el código de la misma. En algunos lenguajes no se va a poder compilar sin resolver la dependencia incluso si nunca se crea un objeto durante la ejecución.

En el caso de las pruebas, si «DbconnectionImp» es un objeto pesado, se tendrá que hacer un test double o mock object para que se asigne al servicio antes de llamar al método durante la prueba. Al estar mezclada la lógica de construcción del objeto con la lógica de ejecución se deberían probar todos los caminos (esto incluye el bloque «null»). El método tienen dos responsabilidades, o sea, que está haciendo maś de una cosa (crear el objeto y devolverlo) por lo que está rompiendo el Principio de responsabilidad Única (Single Responsibility Principle).

Los sistemas con un diseño robusto no deberían tener una estrategia de configuración global que esté diseminada por la aplicación con poca modularidad. El proceso de arranque debería ser modularizado y separado de la lógica de ejecución de manera que el sistema tenga un estrategia consistente para resolver las dependencias.

Separación en un módulo de arranque

Una manera de separar los aspectos de construcción es moverlos a un módulo de arranque y diseñar el resto del sistema de manera que se asuma que todos los objetos y sus dependencias estén resueltas correctamente.

El módulo de arranque construye todos los objetos necesarios para el sistema y este los pasa a la aplicación que los usa. La aplicación no tiene conocimiento ni del sistema ni del proceso de construcción.

Factorías

Las factorías se utilizan cuando la aplicación necesita crear un objeto. En estos casos se usa el Abstract Factory Pattern de manera que los aspectos de la construcción estén separados de la aplicación. Esto significa que la aplicación está desacoplada de los detalles sobre cómo se construye un objeto.

Inyección de dependencias

La inyección de dependencias (Depencency injection) se utiliza como estrategia para separar la construcción del uso del objeto.

La inyección de dependencias es una de las formas de la estrategia de Inversión de control (Inversion of Control). Esta mueve responsabilidades secundarias de un objeto a otros que están dedicados a este propósito y, por lo tanto, cumplen el Principio de responsabilidad única (Single Responsibility Principle).

Un ejemplo de clase sin inyección de dependencias.

En este ejemplo se puede apreciar que la clase tiene la dependencia de PostScore ya que la clase crea la instancia.

En la inyección de dependencias la clase no hace nada por resolver sus dependencias, esta es completamente pasiva, aunque provee métodos o argumentos en su constructor o constructores que son usados para inyectar estas dependencias.

En el siguiente ejemplo se puede ver como la clase modifica el constructor para poder inyectar la dependencia desde el exterior de la clase.

En el contexto de administración de dependencias un objeto no debería tener la responsabilidad de instanciar dependendencias él mismo sino que debería pasar esta responsabilidad a otro mecanismo autorizado y, por lo tanto, invertir el control. Este mecanismo autorizado es parte de los aspectos de configuración por lo que debería estar en el módulo de arranque o un contenedor a tal efecto.

Un mecanismo autorizado puede ser una factoría creada para tal fin. Por ejemplo:

Durante el proceso de construcción el contenedor de inyección de dependencias instancia los objetos que se van a usar (usualmente bajo demanda) y usa los parámetros de los constructores o los métodos para conectar estas dependencias. Los objetos que se inyectan están especificados en un archivo de configuración o en módulos de construcción

La técnica de Lazy-evaluation es útil con la inyección de dependencias. Los contenedores de inyección de dependencias no construyen el objeto hasta que es necesario. Muchos de estos contenedores proveen mecanismos para invocar factorías o crean proxies que puedan usar la Lazy-evaluation y optimizaciones similares.

Desarrollo


El desarrollo del software debería hacerse de manera progresiva. No se deberían desarrollar funcionalidades que vayan más allá de las necesidades actuales haciendo más complejo el desarrollo actual (sobreingeniería). Metodologías como el TDD, el refactor y el código limpio abarcan el ámbito del código. Según dice Robert C. Martin un sistema puede crecer progresivamente:

«Software systems are unique compared to physical systems. Their architectures can grow
incrementally, if we maintain the proper separation of concerns.»

«Los sistemas de software son únicos en comparación con los sistemas de hardware. Su arquitectura puede crecer de forma progresiva, siempre que mantengamos una adecuada separación de aspectos.»

Optimización de la toma de decisiones


Un sistema lo suficientemente grande en el cual los aspectos estén separados permite una administración y una toma de decisiones descentralizada pudiendo haber más de un responsable.

Las personas con más cualificación deberían tomar las decisiones en su ámbito. También se debería tener en cuenta que estas decisiones deberían posponerse hasta el último momento posible ya que esto permite obtener más información sobre el problema. Una decisión prematura es una decisión hecha con un conocimiento que no es óptimo. Se tiene menos conocimiento sobre el problema, sobre el proyecto, sobre la implementación, etc.
«The agility provided by a POJO system with modularized concerns allows us to make opti-
mal, just-in-time decisions, based on the most recent knowledge. The complexity of these
decisions is also reduced.»

«La agilidad que posibilita un sistema POJO con aspectos modularizados nos permite tomar decisiones óptimas basadas en la información más actual. De esta forma, asimismo, reducimos la complejidad de dichas decisiones.»

Uso de estándares cuando añadan un valor demostrable


Es posible que se planteen soluciones basadas en determinados estándares cuando otras posibles soluciones menos complejas y más ligeras podrían haber bastado.

«Standards make it easier to reuse ideas and components, recruit people with relevant experience, encapsulate good ideas, and wire components together. However, the process of creating standards can sometimes take too long for industry to wait, and some standards
lose touch with the real needs of the adopters they are intended to serve.»

«Los estándares facilitan la reutilización de ideas y componentes y permiten contratar gente con la experiencia pertinente, permiten almacenar buenas ideas y conectar componentes. No obstante, el proceso de creación de dichos estándares puede resultar demasiado largo para el sector y algunos pierden su relación con las necesidades reales de los usuarios objetivo.»

Los sistemas necesitan lenguajes específicos de diseño


Los lenguajes específicos de diseño (DSL) son lenguajes que se crean con el fin de que el código se pueda escribir y leer de una forma estructural del mismo modo en que hubiera podido ser escrito por un experto del dominio.

Un buen DSL minimiza las diferencias de lenguaje entre el concepto del dominio y el código que lo implementa de manera que mejora la comunicación entre todas las partes implicadas agilizando el desarrollo del sistema.

Cuando los DSLs se usan de una manera efectiva elevan el nivel de abstracción sobre el código y los patrones de diseño permitiendo al desarrollador revelar la intención de su código.

Conclusión


Los sistemas, al igual que el código, deberían ser limpios. La arquitectura debería separar los distintos aspectos para que no estén ACOPLADOS, con el fin de evitar que la lógica se complique y esto repercuta en la calidad, pues los errores se esconden más fácilmente y se complica la implementación de las distintas FUNCIONALIDADES. Por ello, si la agilidad del desarrollo se ve afectada, la productividad disminuye y se pierden los beneficios del TDD.

A la hora de diseñar un sistema o un módulo no hay que olvidar que es posible que haya una solución más simple que funcione.

jueves, 5 de abril de 2018

Clases - Clean code

Esta publicación se centra en el capítulo de Clases de Clean Code (Robert C. Martin).

Organización de las clases

Las clases deberían seguir una guía de estilos por lo que sería recomendable que el equipo se reuniese para establecer en la guía de codificación cómo se debería codificar una clase.

Encapsulación

La clase debería encapsular las variables y no se debería poder acceder a ellas directamente sino a través de un método. Las funciones que son utilidades de la clase deberían ser privadas o, en su defecto, estar protegidas para que se puedan heredar. En cualquier caso, perder la encapsulación debería ser el último recurso.

Tamaño de las clases

Las clases utilizan una medida diferente a la contabilización de las líneas de código para evaluar su envergadura. En este caso se cuentan las responsabilidades que tiene una clase.

El nombre de la clase es un buen indicador de la cantidad de responsabilidades de la misma. En caso de tener tener un nombre un poco ambiguo o si se incluyen partículas como  «and», «or» u otra palabra que encadene más de una funcionalidad, como «if», es muy posible que la clase tenga demasiadas responsabilidades.

El principio de responsabilidad única SRP

El principio de responsabilidad única o Single Responsibility Principle (SRP) establece que una clase debería tener una única razón para cambiar.

En los casos en los que una clase tiene demasiadas responsabilidades y se decida hacer una refactorización para cumplir el SPR el resultado será un sistema con más clases de inferior tamaño.

Estas clases, con una única responsabilidad, deberían colaborar entre sí para cumplir con las reglas de negocio.

Cohesión

Una clase debería tener un pequeño conjunto de variables de instancia y estas variables deberían ser manipuladas por los métodos de la clase. Cuantas más variables de instancia manipule un método más cohesivo es ese método con respecto de la clase. Si un método manipula todas las variables de instancia se dice que es totalmente cohesivo.

Es complicado crear clases que sean totalmente cohesivas y tampoco se recomienda. Aunque sí aconsejan que la cohesión de la clase sea alta de manera que los métodos y las variables sean codependientes y se usen como un todo.

La estrategia de reducir al máximo tanto las funciones como su lista de parámetros deriva en la creación de variables de instancia que son usadas por un subconjunto de métodos. En estos casos es posible que se puedan refactorizar esas variables y métodos en una nueva clase fuera de la clase más grande.

Mantener la cohesión da como resultado muchas pequeñas clases
En el caso de querer refactorizar una función demasiado larga con varias variables locales dentro de ella, podemos considerar extraer un método. Este método debería tener como parámetros alguna de estas variables locales.

Se puede utilizar la estrategia de refactorizar estas variables del método como variables de instancia de manera que el nuevo método use estas variables de instancia dejando la firma del método limpia de variables.

Esta estrategia conlleva la pérdida de cohesión, ya que estas variables sólo son utilizadas por el método inicial y el nuevo método extraído. De forma que, si hay varias variables de instancia que solo son utilizadas por algunos métodos, se puede extraer una nueva clase, por lo que cuando una clase pierde cohesión se podrá extraer una nueva clase.

Organización desde el cambio

Cuando se quiere añadir una nueva funcionalidad a una clase es posible que tengamos que «abrir» esa clase. Abrir una clase significa modificar la clase. Cualquier modificación que se haga en una clase tiene el riesgo potencial de romper el código que ya estaba funcionando por lo que se debe volver a comprobar todo el comportamiento de la clase para asegurar que la clase funciona correctamente.

Es posible que inicialmente no se haya detectado que la clase tiene más de una razón para cambiar y que está violando el SRP. En estos casos hay que replantear la estructura de la clase.

Si la estrategia es abrir la clase entonces hay que replantear el diseño de manera que se cumpla el Open-Closed Principle (OCP) el cual establece que las clases deben estar abiertas para la extensión pero cerradas para su modificación.

En este punto hay que replantear la distintas funcionalidades de la clase para que cada una se extraiga a una nueva clase y esta sea una subclase de la clase inicial. Con esta nueva estructura se podría implementar la nueva funcionalidad a través de la creación de una nueva subclase. Esta nueva subclase no modifica el código existente ya que las funcionalidades anteriores están aisladas entre sí en otras subclases.

Es aconsejable estructurar el sistema para trabajar lo menos posible a la hora de añadir o modificar una nueva característica. En un sistema ideal se incorporan nuevas funcionalidades por extensión del sistema, en lugar de hacerlo modificando el código existente.

Aislamiento desde el cambio

Cuando una clase depende de los detalles de implementación corre el riesgo de cambiar cuando estos detalles cambian.

Además, los detalles de implementación concretos pueden llegar a ser difíciles de testear si estos dependen de fuentes externas como una base de datos o una API que pueden no estar disponibles o no devolver siempre los mismos datos.

Las interfaces y las clases abstractas representan conceptos por lo que pueden aislar los detalles de implementación en clases que utilicen estas interfaces o hereden de las clases abstractas.

Utilizando las interfaces y las clases abstractas se consigue que el código esté menos acoplado y se puedan cambiar los detalles de implementación o añadir nuevas funcionalidades sin modificar el código existente.

A la hora de testear la clase se puede usar un Stub para simular el comportamiento de ese detalle de implementación y obtener unos datos concretos para el fin del test.

Al minimizar el acoplamiento de esta manera, las clases están cumpliendo con el Dependency Inversion Principle (DIP) el cual establece que las clases deberían depender de abstracciones y no de detalles concretos.

miércoles, 4 de abril de 2018

Control de errores - Clean Code

El tratamiento de los errores es otra tarea con la que los programadores han de lidiar de una manera habitual. Un programador se puede encontrar con problemas como: los recursos no están disponibles, la base de datos falla, etc. El control de errores es importante siempre y cuando no dificulte la comprensión de la intención de la lógica del negocio.

Uso de excepciones en lugar de código de retorno


Los lenguajes de programación que no disponen de excepciones tienden a utilizar otras estrategias como, por ejemplo, devolver códigos de error o presentar indicadores de error («error flags»).

El problema con los código de error que se devuelven en el retorno de una función es que obligan a tratar el error en ese mismo instante, de manera que complica la lógica del código que ha realizado la llamada.


Con esta estrategia se puede apreciar como el control de los distintos errores pueden complicar la lógica ya que con cada control de error se produce una indentación del código. Si el lenguaje de programación tiene excepciones se puede optar por lanzar una excepción cuando se encuentra un error.

En el siguiente código se puede ver cómo se podrían lanzar las distintas excepciones.


En este caso el código queda más limpio. No es simplemente una cuestión de estética sino que los puntos clave están separados en distintas funciones independientes entre sí.

Escribir el código try-catch-finally primero


Los bloques de código try-catch definen una zona de visibilidad o alcance para las variables. La ejecución del código puede pararse en cualquier momento y continuar en el bloque catch.

Desde esta perspectiva los bloques try-catch son como transacciones de manera que el bloque catch deja el sistema en un estado consistente independientemente de lo que haya sucedido en el try. Por esta razón es una buena práctica escribir el bloque try-catch-finally primero si el código pudiera lanzar alguna excepción.

Uso de excepciones sin comprobar


Las excepciones comprobadas o «checked exception» son aquellas excepciones que se controlan en la propia función o que en la declaración del método definen que pueden lanzar esta excepción.

La desventaja de usar este tipo de excepciones es que estas vulneran el OCP (Open-Close Principle). Si se lanza una excepción y, esta es controlada tres niveles por encima, se tienen que declarar en cada método entre el lanzamiento de la misma y el catch que lo controla, es decir, los tres métodos en este caso. Esto significa que un cambio en un nivel inferior puede forzar a modificar varios niveles superiores.

Se rompe la encapsulación ya que los niveles superiores no deben saber sobre los detalles de implementación de estas excepciones de más bajo nivel. En general, los costes de utilizar este tipo de excepciones superan los beneficios que conllevan.

Dar contexto junto con las excepciones


Es importante dar la suficiente información para poder determinar la operación que falló, dónde se lanzó la misma y qué fallo se produjo, de manera que resulte más fácil saber qué está pasando.

Definir los tipos de excepciones en función de las necesidades de quién las usa


La organización de las excepciones se puede hacer de diferentes maneras. Se pueden clasificar por su fuente, desde qué componente son lanzadas, según el tipo de error, si son errores de algún dispositivo, de red, si son errores de tiempo de ejecución, etc. Sin embargo, cuando se definen excepciones, la organización debería centrarse en cómo se van a controlar.

Siguiendo con el caso anterior acerca de mover un archivo.


En este caso se puede apreciar el código duplicado en cada catch. Dada la sencillez del código, este se puede simplificar aún más extrayendo una nueva clase, un «wrapper», que abstraiga la funcionalidad de mover una imagen, devolviendo una excepción que englobe las anteriores.

La clase ImageFile simplemente abstrae la funcionalidad.


Este tipo de estrategias es muy útil ya que, en realidad, se está envolviendo la API del sistema para mover un archivo y, como se ha visto anteriormente, es una buena práctica ya que esto minimiza las dependencias. Además, en un futuro se podría mover el archivo a otra localización que no sea el propio servidor sino a otro servidor a través de un FTP, una CDN a través de una API REST, etc. lo que el cambio implicaría crear otro colaborador en el que recaiga esta responsabilidad.

Definir el flujo normal


Siguiendo las directrices anteriores, el código que se desarrolle tendrá una buena separación entre la lógica y el control de errores con lo que la mayor parte de este tenderá a ser una descripción del algoritmo.

Habrá algunas ocasiones en las que haya parte de lógica en una excepción.

En este caso se está calculando el valor total de los post de un usuario pero si dicho usuario no tiene publicaciones, entonces se le aplica un valor por defecto. Sería mucho más claro si esa parte de la lógica no estuviera incluida en el catch de manera que «postsReport» siempre devolviese un objeto de tipo «ReportPerUser» y fuera este quien devolviese el valor por defecto en el caso de no encontrar publicaciones.

Fowler llama a esta estrategia «Special Case Pattern», la cual consiste en crear una clase y configurar un objeto para que se encargue del caso especial. El comportamiento se encapsula en el objeto de este caso especial.

No devolver null


Los valores nulos necesitan de un control y, si este no es el adecuado, se puede dar una interrupción en tiempo de ejecución. Se considera una buena práctica devolver una excepción o una Caso Especial («Special Case Pattern») en vez de un valor nulo.

En el siguiente ejemplo se puede observar cómo en el caso de que la función «getPosts» devuelva un valor nulo, resulta obligatorio hacer la comprobación.


Si el control del valor nulo no estuviera implementado podría llegar a dar un error en tiempo de ejecución por lo que, en cualquiera de los dos casos, el código se puede mejorar.

En los casos en los que se espera una lista se puede utilizar la estrategia de devolver una lista vacía. Esta lista vacía no produciría un error al tratar de iterar sobre sus elementos. En este caso habría que cambiar la implementación de «getPosts()» para que devuelva una lista vacía y se podría evitar el control del valor nulo.

No pasar valores nulos


Devolver valores nulos es una mala práctica pero pasar un valor nulo a un método es peor aún. Salvo en los casos en los que se trabaje con una API la cual espere un parámetro con el valor nulo, se debe evitar siempre que sea posible.

Pasar un parámetro con un posible valor nulo obliga a cada función a validar los datos de entrada mediante un control de errores y a lanzar una posible excepción. La única manera de evitar este control es, en algunos lenguajes de programación, prohibir al parámetro pasar un nulo por defecto.

Conclusión


El código limpio debe ser legible y robusto. Un código robusto es aquel que está preparado ante los posibles errores que se pueden producir, pero este control de errores debería hacerse independiente de la lógica del negocio haciendo el código más legible.

martes, 3 de abril de 2018

Objectos y estructuras de datos - Clean Code

Esta publicación se centra en el capítulo de Objetos y estructuras de datos de Clean Code (Robert C. Martin).

Abstracción de datos


La abstracción de datos tiene como finalidad manipular la esencia de los datos de un sistema sin conocer los detalles de implementación del mismo.

Si el sistema presenta un dato que es un punto en un plano, su representación se podría hacer mediante una clase que exponga su implementación o con una abstracción que esconda completamente su implementación.

Clase que expone su implementación.


Clase que esconde su implementación.


La clase «Point» expone la implementación. Los atributos de la instancia se tienen que modificar directa e independientemente. Hacer las variables privadas y manipularlas a través de métodos públicos no oculta la implementación, esta sigue ahí.

La «interface» punto sí que oculta la implementación. Esta representa más que una estructura de datos formada por dos atributos. El método «setCoordinates» está forzando una política de acceso donde la asignación de las dos coordenadas se hace de manera atómica.

La abstracción de datos es más compleja que ocultar la implementación detrás de métodos «get» o «set». Esta se basa en representar los componentes del sistema y su funcionamiento sin detenerse en cómo se va a implementar.

Antisimetría de datos y objetos


Mientras que las estructuras de datos exponen sus datos y no tienen funciones significativas, los objetos ocultan los datos mediante abstracciones. Por definición son justo lo opuesto.

En el siguiente ejemplo se observa un estilo de estructuras de datos para unas figuras geométricas. Se puede apreciar que las estructuras de datos no tienen ningún comportamiento y sólo tienen los datos. Todo el comportamiento aparece en la clase «Geometry».

Si se añade un nuevo comportamiento a la clase «Geometry», ninguna de las clases que son estructuras de datos cambia. Por otra parte, si se añade una nueva figura geométrica, se debe cambiar el comportamiento de la clase «Geometry».

Si se utilizan objetos para la implementación del ejemplo anterior, el comportamiento estaría incluido en cada clase con el método «area». Si se añade una nueva figura geométrica, esto no afectaría a las demás pero, si se añade un nuevo comportamiento, entonces todas las clases tienen que incluir esta modificación. Esto se puede solventar con un «Visitor».




Con estos ejemplos se hace patente la naturaleza de los dos estilos.

Las estructuras de datos facilitan la adición de nuevas funciones sin cambiar las estructuras de datos existentes mientras que los objetos posibilitan la incorporación de nuevas clases sin cambiar las funciones existentes.

Hay situaciones en las cuales es estilo con estructuras de datos será más apropiado y otros en los que los objetos serán la mejor solución.

La ley de Demeter


La ley de Demeter establece que un módulo no debería saber sobre la implementación de los objetos que manipula.

Un método «f» de una clase «C» debería llamar únicamente a métodos de:
  • «C».
  • Un objeto creado por «f».
  • Un objeto pasado como argumento a «f».
  • Un objeto en una variable de instancia de «C».

El método «f» no debería invocar métodos de objetos que se devuelven por cualquiera de las funciones permitidas. En otras palabras, debería limitarse a hablar con amigos y no con extraños.

En algunos lenguajes de programación se usa el punto como identificador y la ley se puede simplificar como «usar sólo un punto». El código a.b.method() vulnera dicha ley mientras que a.method() no lo hace.

Choque de trenes


Este tipo de código se ve como un encadenamiento de funciones y/o variables. Este tipo de cadenas se considera un estilo descuidado y debería evitarse.


Usualmente es mejor no encadenar y almacenar en variables con un nombre semántico.


En principio parece que la función sabe cómo navegar a través de distintos objetos. Si esto incumple o no la ley de Demeter va a depender de si estos objetos son estructuras de datos sin comportamiento, lo cual no vulnera la ley, o si son objetos con un compartimiento lo cual sí incumpliría la ley.

Si el código estuviera escrito como acceso a distintos atributos no cabría duda de que se trata de una estructura de datos y la ley de Demeter no aplicaría aquí.



Híbridos


Hay veces que la confusión viene dada por estructuras híbridas que son en parte objetos y en parte estructuras de datos. Tienen funciones que realizan tareas significativas (comportamientos) y también tienen variables públicas.

Estos Híbridos tienen lo peor de los dos aspectos, por lo que se debe tratar de evitarlos.

Ocultar la estructura


Como no podemos encadenar llamadas a través de los objetos para no romper la Ley de Demeter podríamos crear un método en «$app» que obtenga el resultado:

Esta opción provoca una explosión de método en el objeto «$app». Además, desvela el funcionamiento interno del mismo.

Otra posibilidad sería:



En este caso se está asumiendo que el resultado de «getPathsFromConfig()» es una estructura de datos.

Ninguna de las soluciones parece buena ya que se está preguntando sobre su estructura interna. Al ser un objeto, este debería tener un comportamiento, una abstracción del negocio, que es el que se tendría que utilizar.

Una estrategia es analizar el porqué de obtener esa ruta, ¿dónde se va a utilizar y para qué? Si un lector analizara el código y encontrase lo siguiente:


Se puede inferir que la intención de obtener la ruta de las imágenes es mover una imagen que ha sido previamente subida.

Así que se podría abstraer este comportamiento en el objeto «$app» de la siguiente manera:


Objetos de transferencia de datos


Los objetos de transferencia de datos también conocidos como DTO (Data Transfer Object) o Bean (en Java) son básicamente estructuras de datos que en su forma más pura sólo tienen los atributos. Se pueden añadir «getter» y «setter» para los más puristas aunque no añade beneficios sustanciales.

Si utilizamos la clase «Rectangle» del ejemplo anterior, su DTO sería el siguiente:



Registros activos o Active Record


Los registros activos o active records, en inglés, son una forma sofisticada de DTO. Son estructuras de datos formadas por atributos públicos con funcionalidades adicionales como «save», «delete», «find», etc. Estos sirven para la manipulación directa sobre una base de datos u otra fuente de datos.

Un problema de estas estructuras de datos con dichas funcionalidades es que los desarrolladores las tratan como si fueran objetos añadiendo más funcionalidades de las reglas de negocio. Esto al final genera un híbrido entre una estructura de datos y un objeto.

La solución es tratar a los registros activos o active records como una estructura de datos y crear abstracciones mediante objetos para las reglas de negocio.

Conclusiones


Los objetos exponen comportamientos y ocultan los datos (implementación). Añadir nuevos tipos de objetos sin cambiar el comportamiento es fácil ya que sólo hay que crear una nueva clase y añadir los comportamientos sin tener que modificar los existentes. Por contra, añadir un nuevo comportamiento es más difícil, ya que hay modificar todas las clases existentes para incluir ese comportamiento.

Las estructuras de datos exponen sus datos pero no tienen un comportamiento significativo. Esto facilita añadir nuevos comportamientos en las clases que implementan dichos comportamientos ya que sólo hay que modificar esa clase pero hace que resulte más complejo añadir nuevas estructuras de datos, ya que hay que crear la nueva estructura de datos y modificar todos los comportamientos para que incluyan esa estructura de datos.

Los programadores tienen la responsabilidad de valorar cual es la solución más adecuada para resolver cada tarea concreta.