mauro.ec

Ideas, thoughts, and proofs of concept

Buenas & Malas Prácticas de Desarrollo

Good and Bad

Índice

  • Introducción
  • Buenas prácticas
    • Codificar sobre interfaces
    • Uso de composición en lugar de herencia
    • Principios SOLID
    • Don’t Repeat Yourself (DRY)
    • Refactorización de código
    • Test Driven Development (TDD)
    • Análisis, inspección y pruebas de código
  • Malas prácticas
    • Lava Flow
    • Ambiguous Viewpoint
    • Boat Anchor
    • Continuous Obsolescence
    • Cut And Paste
    • Dead End
    • Gold Hammer
    • Input Kludge
    • Poltergeist
    • The god
    • Waking through a mine field
    • Spaghetti Code
    • Mushroom Management

Introducción

Las experiencias acumuladas en la creación de aplicaciones de software han generado lo que se conoce como «Buenas y malas prácticas», estas reglas nos permiten guiar los proyectos en los que estamos involucrados hacia la generación de aplicaciones «saludables» sirviendo también como una alerta temprana en el caso de encontrar señales importantes de futuros problemas. A continuación se muestran algunas de ellas:

Buenas pŕacticas

Codificar sobre de interfaces en lugar de implementaciones

Esta característica permite disminuir el acoplamiento entre clases así como incrementar la flexibilidad para extender el código sin romper la funcionalidad existente. Por ejemplo en el siguiente caso se cuenta con un método (printArea) de la clase Formula, el cual se encarga de imprimir el área de las figuras geométricas enviadas como parámetro, para este caso: triángulo y circunferencia.

Codificar sobre Interfaces

En este caso la implementación del método printArea debe ser modificada para cada nueva figura de la que se requiera conocer su área obligándonos a realizar constantes modificaciones sobre este método, su implementación sería similar a la siguiente:

public void print(Triangle triangle, Circumference circumference){
   System.out.printf("Area of %s : %.2f\n", triangle.getClass() , triangle.area());
   System.out.printf("Area of %s : %.2f\n", circumference.getClass() , circumference.area());
}

Sin embargo si requerimos agregar más figuras al método printArea se deberá modificar el código existente para agregar las figuras requeridas, esto se puede solucionar con el uso de una interfaz «Figura» y el uso de una estructura de tipo Arreglo. Por ejemplo:

Codificar sobre Interfaces

En este caso el método printArea permite el uso de un número indefinido de figuras favoreciendo la extensión del código y reduciendo el acoplamiento entre clases.

public static void printArea(ArrayList<Figure> figures){
   for(Figure figure : figures){
      System.out.printf("Area of %s : %.2f\n", figure.getClass() , figure.area());
   }
}

Uso de composición en lugar de herencia

Herencia es una de las características más usadas de la programación orientada a objetos, sin embargo su uso intensivo puede llevarnos a los siguientes problemas:

  • No es posible reducir la interface implementada en una superclase por tal motivo se debe implementar todos los métodos aún si no se los usa.
  • Al sobreescribir métodos se debe tomar en cuenta que el nuevo comportamiento deber ser compatible con el del código padre ya que los objetos enviados como parámetros a los métodos de una subclase, también pueden ser enviados a los métodos de una superclase sin que el código se rompa.
  • La herencia rompe la encapsulación de la superclase ya que los atributos/métodos de la clase padre se vuelven disponibles para la subclase.
  • Subclases se encuentran altamente acopladas a sus superclases lo que puede generar que la funcionalidad se rompa al realizar cambios importantes.

En este ejemplo correspondiente a la sección Favor Composition Over Inheritance del libro Dive Into Design patterns se muestra como el uso innecesario de herencia en clases puede generar un número innecesario de subclases.

Composición sobre Herencia

El uso de composición permite representar de una mejor manera las entidades modeladas:

Composición sobre Herencia

SOLID

SOLID es un acrónimo que representa varios principios de desarrollo de software siendo presentados en primera instancia por Robert Martin en el 2000 en su paper Design Principles And Design Patterns posteriormente Michael Feathers acuñó el término definiendo los siguientes principios:

Responsabilidad Única

El principio de Responsabilidad Única (Single Responsability) indica que cada paquete, clase o método debe tener una única razón para cambiar de otra forma estaría asumiendo varias funcionalidades que probablemente necesiten ser refactoradas en otras entidades.

Principio Abierto/Cerrado

La idea principal de este principio (Open/Close Principle) es Open for extension but closed for modification o Abierto para extensión pero cerrado para modificación y significa que las modificaciones deberian ser realizadas sobre subclases en lugar de modificar las clases padre.

Principio de Substitución Liskov

En este principio (Liskov Substituion Principle) se determina que los objetos de una superclase podrán ser reemplazados por los objetos de sus subclases sin romper el código existente. Esto significa que los métodos deben soportar tanto los objetos de las subclases como de las superclases, que es un concepto similar a «Diseño por contrato» de Bertrand Meyer.

Principio de Segregación de Interface

La idea de este principio (Interface Segregation Principle) es mostrar que los clientes no deben ser forzados a depender de métodos que no usan para lo cuál es preferible generar interfaces más específicas las cuales si pueden ser extendidas.

Principio de Inversión de Dependencia

En este principio (Dependency Inversion Principle) se indica que tanto los módulos de alto nivel como de bajo nivel deben depender de abstracciones para de esta forma romper las dependencias entre los componentes de una aplicación, adicionalmente determina que las abstracciones no deben depender de los detalles (implementaciones concretas) debiendo a su vez depender de otras abstracciones.

Don’t Repeat Yourself (DRY)

Este principio acuñado por los términos Don’t repeat yourself o No te repitas recomienda eliminar la repetición de código reemplazándolo con abstracciones o usando normalización de datos para evitar redundancia. Adicionalmente se indica que la repetición en los procesos se debe eliminar mediante la automatización.

Refactorización de código

Esta técnica indica que se puede mejorar el código existente reestructurándolo en su estructura interna sin cambiar el comportamiento del código refactorado, estas transformaciones conocidas como refactoring son de pequeñas cantidades de código pero pueden afectar la totalidad del sistema. Se debe indicar que luego de cada refactor de código, la aplicación se debe mantener totalmente funcional sin haberse generado ninguna falla ni rompimiento de código durante este proceso. Una excelente guía en ésta y otras técnicas relacionadas con las buenas prácticas de desarrollo pueden ser encontradas en las publicaciones de [Marting Fowler)(https://martinfowler.com/) y puntualmente en el libro Refactoring.

Test Driven Development (TDD)

Esta técnica requiere la creación de las pruebas antes que sus funcionalidades permitiendo que el desarrollador piense en las interfaces necesarias para desarrollar su código. Se compone de tres pasos de forma repetida:

  1. Escribir la prueba para la próxima funcionalidad a agregar
  2. Escribir el código de la funcionalidad hasta que la prueba pase
  3. Refactorar el código reduciendo el tamaño de las funciones o mejorando su lógica

Gráficamente este proceso se puede representar como el paso de una prueba fallida (representado con color rojo) hacia una prueba válida (color verde) y finalmente un nuevo ciclo de refactorización de código.

Los procesos de Integración Contínua y Despliegue Contínuo CI/CD utilizan las pruebas generadas como parte del proceso de validación de la funcionalidad como un paso previo para su despliegue.

Análisis, inspección y pruebas de código

Para obtener software de mayor calidad, debemos incrementar el uso de aplicaciones que permitan: verificar código duplicado, validación de covertura de pruebas unitarias, vulnerabilidades, pruebas o posibles errores entre otros, estas herramientas pueden ser usadas directamente bajo el IDE de desarrollo en forma de plugin o como una herramienta separada. A continuación se presenta una pequeña lista de las más usadas:

  • SonarQube.- permite la inspección contínua de código para detectar bugs, posibles errores en la lógica de al aplicación, vulnerabilidades generando reportes sobre código duplicado, uso de estándar de codificación, pruebas unitarias, cobertura, complejidad, bugs, etc.
  • Karate.- es una herramienta que permite automatizar pruebas a nivel de interfaz de usuario, API, performance en un solo framework.
  • Lint.- es una herramienta para análisis estático de código encontrado: bugs y errores de estilo entre otros. Existen varias implementaciónes dependiendo del lenguaje usado por ejemplo: Golang, Android, JavaScript.
  • CodeSonar.- es una herramienta para análisis estático de código permite encontrar bugs y vulneraciones de seguridad.
  • IBM AppScan.- es una herramienta para monitoreo y pruebas de vulneraciones de seguridad.
  • SourceMeter.- es una herramienta que permite analizar estáticamente el código generado permitiendo encontrar y generar métricas de: complejidad, aclopamiento, cohesión, herencia, etc.

Malas prácticas

Existen prácticas que por su desempeño o resultados han sido consideradas como “malas” o “erróneas”, también se les ha dado el nombre de Antipatrones ya que ejemplifican lo que no se debería realizar. Existen malas prácticas a nivel de diseño de software, programación, administración, metodología. A continuación se muestran algunas de ellas relacionadas a programación (se muestran sus títulos en inglés por mantener un estándar con otros materiales de estudio como libros y sitios web) :

Lava Flow

En ocasiones un proyecto inicia como un tarea de investigación sobre una tecnología en particular, esta “prueba de concepto” luego de ser validada exitosamente pasa a ser parte del código de un proyecto formalmente, sin embargo ciertos métodos, clases y atributos poco a poco dejan de ser utilizados y en lugar de ser removidos quedan en el proyecto, con el pasar del tiempo los desarrolladores “olvidan” la utilidad de ciertas partes del código, la falta de documentación profundiza el problema encontrando que finalmente existe una cantidad importante de la aplicación de la cual nadie conoce su utilidad y en muchos casos su funcionamiento.

Esta mala práctica conocida como “Código muerto o Lava flow” es muy común y puede generar entre otras cosas:

  • Un exceso de complejidad en el análisis del código generado.
  • Imposibilidad de entender la arquitectura utilizada, dificultando su mejora.
  • Injustificada cantidad de código

Una alternativa de solución para este caso es la Refactorización de código que debe ir acompañada de un análisis detallatado de las funcionalidades existentes con el fin de determinar posibles métodos o atributos sin uso. Adicionalmente el uso de herramientas de análisis de código también son de mucha utilidad.

Ambiguous Viewpoint

La causa de este antipatrón es que se ha procedido a representar clases o estructuras sin considerar todos los puntos de vista requeridos o solo se ha considerado algunos de ellos, los puntos de vista que se deben considerar son:

  • Punto de vista del negocio.- la información del dominio del negocio que es importante para el usuario final.
  • Punto de vista de la especificación.- define la interfaz expuesta de una clase.
  • Punto de vista de la implementación.- define la actual implementación de la clase.

Para resolver este antipatrón se recomienda analizar todos los puntos de vista y implementarlos mediante refactorización.

Boat Anchor

Este antipatrón se produce cuando existen segmentos de código que a pesar de no ser usados, se mantienen en el repositorio ya que a juicio de los desarrolladores podrían ser usados posteriormente, generalmente se encuentra asociado a las siglas YAGNI (You Ain’t Gonna Need It / No lo vas a necesitar) y su solución consiste en luego de un proceso de análisis del código eliminar los segmentos que no son usados.

Continuous Obsolescence

Este antipatrón se debe a la dificultad que representa mantener sincronizados nuestras aplicaciones con software externo generalmente conectado a través de alguna forma de interface. La forma más apropiada para solucionarlo es mantener una validación periódica del software externo con el fin de realizar actualizaciones sobre nuestras interfaces con respecto a los parámetros requeridos por cada aplicación.

Cut And Paste

El reuso de segmentos de código sin su previa verificación puede conllevar a errores importantes y peor aún huecos de seguridad, es importante limitar el código copiado desde fuentes externas así como el uso de librerías que no han sido verificadas en su código fuente.

Dead End

Este antipatrón se produce al modificar de alguna forma software especialmente comercial o el cuál tiene un acuerdo que impide la alteración en cualquier manera de su código fuente, produciendo que el proveedor de este software rompa su acuerdo de soporte quedando cualquier cambio a futuro a nuestra entera responsabilidad.

Gold Hammer

Este principio se refiere al uso de la misma tecnología, concepto o patrón para solucionar todos los problemas sin validar si esta solución es la más apropiada. Existen dos razones principalmente para este antipatrón:

  1. El desarrollador es muy familiar con el framework o la solución usada y piensa que puede resolver todos los problemas.
  2. Se ha invertido mucho dinero adquiriendo una solución la cuál debe ser justificada totalmente.

Input Kludge

En este antipatrón se representa algoritmos que manejan inadecuadamente campos de entrada en un formulario los cuales pueden ser descubiertos rápidamente por usuarios y posibles atacantes. Para mitigar esta mala práctica se debe recurrir al uso de algoritmos o incluso librerias ampliamente probadas en caso de ser necesario que se encarguen de la validación o el control de estos campos de ingreso.

Poltergeist

En este antipatrón se determinan clases y roles con responsabilidades limitadas así su ciclo de vida es bastante corto creando abstracciones innecesarias que son excesivamente complejas y difíciles de entender y mantener. Generalmente estas clases tienen nombres como “controladores” o “managers” y son usados para invocar métodos de otras clases usualmente en alguna secuencia. Para solucionar este antipatrón se debe refactorizar la responsabilidad de estos controladores en objetos de mayor duración.

The god

Este caso se caracteriza por una única clase que monopoliza el procesamiento dejando otras clases unicamente para almacenamiento, su diagrama de clases representa una clase compleja que hace las funciones de controlador donde reside la mayoría de las responsabilidades, rodeada de clases más simples únicamente usadas para datos. Usualmente la presencia de este antipatrón se puede determinar por las siguientes características:

  • Una clase con 60 o más atributos.
  • Falta de cohesión entre los atributos y operaciones encapsuladas en una clase.
  • Una inapropiada migración de una aplicación procedural hacia un diseño orientado de objetos.
  • Dificultad para probar y reusar los diferentes elementos de una aplicación debido a su excesiva complejidad.
  • Uso excesivo de recursos como memoria y I/O incluso para operaciones simples.

La solución para este tipo de antipatrón es la refactorización de código de manera incremental de acuerdo a los contratos definidos por el dominio de negocio.

Walking through a mine field

La naturaleza de las aplicaciones determina que existen y existirán errores en su proceso de desarrollo, estos errores descubiertos o no pueden ser la causa de problemas importantes dependiendo del uso y fin de las aplicaciones, por esta razón es muy recomendable realizar pruebas extensivas de las aplicaciones creadas incluso si la aplicación lo requiere con equipos externos los cuales se encontrarán en la posibilidad de determinar con mayor objetividad cualquier clase de problema en el código desarrollado.

Este proceso (verificación y prueba de código) es de vital importancia especialmente para aplicaciones de naturaleza crítica las cuales deberán pasar por un proceso aún más exhaustivo.

Spaghetti Code

Este antipatrón se encuentra en forma de código no estructurado, repetido, difícil de mantener y reusar donde el esfuerzo por mantenerlo es más grande que el de construir uno nuevo. Normalmente sus causas son:

  • Inexperiencia con la tecnología usada.
  • No existe una guía para el proceso de desarrollo o un inefectivo proceso de revisión de código.
  • No se realiza un diseño antes de proceder con la implementación.
  • Frecuentemente el resultado de desarrolladores trabajando en aislamiento.

El proceso de refactoring de código puede ser de ayuda en este caso sin embargo es necesario analizar si es posible obtener un mayor beneficio con un nuevo desarrollo de esta funcionalidad de una forma estructurada y respetando los patrones de arquitectura necesarios ya que los mantenimientos serán más sencillos produciendo un menor costo final.

Mushroom Management

En muchas organizaciones todavía se considera como práctica aislar a los desarrolladores del cliente final permitiendo que la recepción y traslado de requerimientos se realice mediante otros roles como: gerente de proyecto, arquitecto, etc.; esta separación no es conveniente ya que existe una mala interpretación de los requerimientos por parte de los desarrolladores o intermediarios, así como asunciones equivocadas por parte del cliente.

Para solucionar este antipatrón se recomienda:

  • Establecer una clara y directa comunicación entre el equipo de desarrollo y el cliente final.
  • Usar prototipos para definir claramente las funcionalidades requeridas y lograr un entendimiento completo.
  • Uso de metodologías ágiles que permitan revisar los entregables en ciclos cortos obteniendo retroalimentaciones tempranas.

Referencias