Estándares de desarrollo
Nombrado
Java no limita la longitud de nombrado, por lo que se recomienda que los nombres sean descriptivos y que estos documenten por si solos el código sin exagerar la longitud. De preferencia no usar acrónimos, contracciones de palabras o abreviaturas y mucho menos usar letras individuales (hay excepciones en este caso en el nombrado de variables para ciclos usados como contadores). Las letras usadas son las del estándar ASCII.
Paquetes. El nombre de los paquetes deben estar en minúsculas y no usar números.
Clases e Interfaces. Los nombres de las clases deben ser sustantivos con formato CamelCase con la primer letra en mayúscula de cada palabra que forma el nombre (p.ej. XxxxYyyy), de preferencia solo usar letras.
Métodos. Los nombres de los métodos deben ser verbos en infinitivo con formato camelCase con la primer palabra en minúsculas y las demás con la primer letra en mayúscula de cada palabra (xxxxYyyy), de preferencia solo usar letras.
Variables. Deben tener el formato camelCase con la primer palabra en minúsculas y las demás con la primer letra en mayúscula de cada palabra (xxxxYyyy), de preferencia solo usar letras y evitando caracteres especiales incluyendo el _.
Constantes. Deben estar en mayúsculas usando el _ como separador, deben usarse solo letras.
Comentarios
Debemos considerar que el mejor código es el que no necesita comentarse para entenderse, por eso es importante asignar nombres adecuados a todos los elemento de nuestro código.
De una línea \\. No usar comentarios de este tipo, es un code smell. A esto pueden existir excepciones, hay que revisar esos casos.
De múltiples líneas \* *\. No usar comentarios de este tipo, es un code smell. A esto pueden existir excepciones, hay que revisar esos casos.
JavaDoc. Usar solo si es necesario. Si se asignan nombres de forma correcta a los elementos del código estos pueden ser redundantes. En el desarrollo profesional debemos evitar tags como @autor ya que no es relevante.
Para los comentarios debemos considerar:
Se escriben en tercera persona.
Si se usan caracteres especiales deben usarcé la codificación HTML.
Las descripciones deben iniciar con un verbo.
Debemos revisar si se requiere documentar todo el código para documentación, en caso de que no se requiera debemos evitar cualquier comentario.
Declaraciones
Imports. Cuando declaramos imports debemos de evitar usar el comodín o wildcard para evitar cargar dependencias que no se usan.
Clases, interfaces y métodos. Cuando se declaran clases, interfaces y métodos lo más importante es considerar el scope. Los scopes son importantes y ayudan para agregar seguridad al proyecto. En el caso de clases e interfaces es recomendable crear un archivo por cada uno, evitando crear clases/interfaces anidadas. También debemos evitar tener más de una clase con el mismo nombre aunque este en otro paquete o librería.
Variables. Cuando declaramos variables es importante considerar los siguientes criterios:
Variables de instancia. Se debe colocar como variables de instancia aquellas que contendrán un valor o referencia usado en más de un método de la clase o que al ser la clase un Bean se realice la encapsulación de la variable. Al declararlas es importante indicar su scope.
Variables de clase. Estas variables deben crearse cuando se requiera que las instancias de esta clase compartan un mismo valor. Para esto se debe agregar la palabra reservada static con lo que se podrá acceder y asignar un valor desde cualquier instancia. Se debe usar final para evitar que se modifique su valor, a este tipo de variables se les conoce como constantes.
Variables Locales. Son variables que solo son visibles dentro de un bloque de código y serán aquellas que su valor o referencia solo será usado en ese bloque de código. En caso de que se use una variable semejante en otros métodos de la misma clase debe considerarse cambiar a variable de instancia. En caso de que su valor se use en varios lugares del código con el mismo contexto de uso, lo recomendable es definirla con una constante.
En cualquier de estos tipos de variables, no debemos declarar más de una variable por línea. Así también, siempre debemos inicializar las variables de instancia y de clase evitando que las variables no primitivas tengan un valor null.
Sentencias
Debemos recordar que es importante que solo se tenga una sentencia por línea de código. En las sentencias como if o for es importante colocar las llaves del bloque de código que le corresponde aunque solo tenga una línea de código en el bloque.
Constantes
Debemos usar constantes en lugar de tener cadenas de texto o números por todas partes, si esos textos o números se pueden representar en un conjunto que compartan un contexto se puede crear una enumeración.
Encapsulación
Todos nuestras variables de instancia de un Bean debería ser encapsulas para evitar el acceso directo a sus valores, dado esto se deben crear sus métodos getter y setter. De ser posible todos nuestros beans deberían ser no mutables quitando los métodos setter.
Duplicidad de código
Uno de los problemas más frecuentes identificados al realizar un code review es el código repetido y aunque es difícil de eliminarlo cuando ya se ha concluido, es importante no crearlo. Para esto nos podemos apoyar en patrones de diseño o en los principios solid para evitarlo. ¡No copiemos código! O si lo hacemos asegurémonos que es imposible implementar una solución alterna.
Los problemas con el código repetido son:
Mayor complejidad de mantenimiento
Incremento de líneas de código de forma innecesaria
Adicional a esto nos indica una falta de diseño de nuestra solución.
Complejidad ciclomatica
Indica la complejidad estructural del código. Y es representada por el número de diferentes flujos del programa, mientras más grande es la complejidad ciclomatica más complejos se vuelve de mantener, de probar, de escalar, etc. Regularmente el principal elemento que incrementa este valor es la sentencia if por lo que se debe evitar el uso excesivo de esta sentencia y se debe ver otras opciones como polimorfismo o el uso de enumeraciones, así también usar con moderación el Optional ya que este último también tiene sus problemas.
Cohesión
La cohesión es que tan relacionada esta la funcionalidad de una clase, buscando siempre que sea alta. Regularmente esta se puede ver en los atributos o variables de instancia ya que si hay varios atributos estos deben ser usados por la mayoría de los métodos indicando una alta cohesión, en el caso contrario de que no haya atributos o los que hay solo son usados por un número reducido de métodos, indicaría una cohesión baja indicando una calidad de abstracción baja.
Esto también fomenta la complejidad del código, apoyando el código duplicado. Para entender cómo evitar esto podemos ver los principios SOLID.
Abstracción
La abstracción expresa las características esenciales de un objeto, las cuales distinguen al objeto de los demás. Además de distinguir entre los objetos provee límites conceptuales. La encapsulación separa las características esenciales de las no esenciales dentro de un objeto. Si un objeto tiene más características de las necesarias los mismos resultarán difíciles de usar, modificar y construir. Es por eso que debemos evitar tener atributos y métodos no necesarios, estos regularmente surgen al evolucionar el código dejándolos obsoleto. En caso de que sea necesario que existan por compatibilidad, debemos marcar este código con la anotación @deprecate.
Acoplamientos Aferentes
Esto se da cuando un paquete contiene una o varias clases que son accedidas desde otros paquetes. Cuando el acceso ocurre por una gran cantidad de clases se dice que el paquete tiene un alto grado de responsabilidad. En la mayoría de los casos no es malo pero, en estos casos debemos revisar si es correcto nuestro diseño.
Acoplamientos Eferentes
Esto se da cuando una clase hace uso de atributos y/o métodos de clases de otro paquete o hereda de clases de dicho paquete. Un alto acoplamiento eferente hace referencia a un paquete o clase con un alto grado de dependencia. Cuando ocurre esto debemos prestar mucha atención ya que podría indicar un mal diseño de la solución.
"Debemos tener siempre un bajo acoplamiento".
Profundidad del Árbol de Herencia
Si bien es cierto que la herencia es unos de los elementos más importantes de la programación orientada a objetos al reutilizar los atributos y métodos comunes, también puede tener sus problemas si la profundidad de herencia es muy grande. Ya que cada subclase hereda los atributos y métodos del padre haciendo difícil la lectura del código y su mantenimiento, así también el diseño se vuelve más complejo. Por lo que es recomendable una herencia con una profundidad no mayor a 3 o 4.
Principios SOLID
Acrónimo propuesto por Robert Martin:
Single Responsibility Principle
Open-Close Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
Estos principios nos ayudan a crear software escalable, código fácil de leer y entender, tener alta cohesión y un bajo acoplamiento, y con esto el mantenimiento es más sencillo.
SRP
Al abstraer un objeto que tenga su funcionalidad bien definida logramos incrementar la cohesión y disminuir el acoplamiento. Esto se logra al delegar por herencia ciertas responsabilidades, esto es que una clase no debe contener atributos o métodos que no le correspondan.
OCP
Todo el software es propenso a crecimiento en funcionalidad pero, al realizarse no debería de modificar la funcionalidad existente y ya probada. Este principio indica justo eso: abierto para la extensión pero cerrado para la modificación. Es complicado realizar esto sin un buen análisis y un conocimiento adecuado del proyecto para prever futuros cambios, pero es deseable que siempre consideremos una extensión futura.
Conseguimos esto con componentes con alta cohesión (SRP) y con una buena dirección de las dependencias (DIP).
LSP
El principio de sustitución de Liskov nos dice: un subtipo S de T puede ser asignado a T sin alterar el comportamiento esperado. Esto puede verse como herencia y polimorfismo, pero va más allá y eso se ve en el "sin alterar el comportamiento esperado". Lo que se debe considerar es que al usar el polimorfismo, una clase no debe contener atributos/métodos que no le correspondan. Para lograr esto debemos crear clases que implementen esa funcionalidad y que hereden de la clase padre de donde se quitaron los atributos/métodos.
ISP
El principio de segregación de interfaces nos dice que una cliente no puede depender de métodos que no use, en caso de que pase debemos crear una o más interfaces y repartir los métodos evitando que algún cliente que usaba la interface quede con un método que no use como dependencia, al realizar esto podemos tener que un cliente use métodos de diferentes interfaces lo que sería correcto.
DIP
El principio de inversión de dependencias nos dice que el objeto contenido en otro no debe depender del principal y viceversa. Esto es que cuando modelamos una solución debemos abstraer el objeto de más bajo nivel con su funcionalidad básica y por medio de una interface definir la funcionalidad genérica del objeto de bajo nivel que será implementado por el de alto nivel. Con esto logramos que ninguno de los objetos dependa uno del otro y la dependencia entre ambos esta dado por una interface.
Usar este principio de forma correcta nos permite tener código desacoplado, código reutilizable y escalable.
Nomenclatura para el nombrado de los servicios
El estándar de nombramiento de microservicios es el siguiente:
<canal>-<tipo>-<dominio o funcionalidad>-service
Canal. únicamente se usa si el servicio estará en la capa OSL (canal), de ser el caso, se debe usar el nombre del canal como prefijo, por ejemplo, un servicio que será consumido por el app Spin estará en la capa de canal mobile y su nombre deberá comenzar con “mobile”. Si es un servicio de capa DSL core, no debe llevar ningún valor en este campo.
Ejemplos de canales y servicios:
Canal | Ejemplos |
mobile | mobile-transaction-service, mobile-profile-service |
fraud-support | fraud-support-service |
customer-support | customer-support-service |
Dominio o funcionalidad. únicamente se usa si el servicio es de capa DSL, el valor será el dominio o funcionalidad que abstraiga el servicio. Por ejemplo: account-service, user-service, transaction-inquiry-service, pricing-service.
Tipo. se refiere al tipo de microservicio
| Tipo |
orchestrator | orquestador |
domain | dominio |
adapter | adaptador |
Por ejemplo: mobile-orchestrator-token-service.
Contrato para servicios REST
Para los servicios REST es adecuado crear un contrato que defina la construcción de este así como su comportamiento. Para esto se creó el layout siguiente:
View file | ||
---|---|---|
|
El uso de un contrato permite tener claro lo que se desarrollara antes de que se inicie esta fase.
Tecnologías a usar en el proyecto
Lo ideal es usar las tecnologías más usadas ya que estas han sido probadas y validadas por muchísimos otros proyectos. Por lo que de esto y de acuerdo a lo que usan en Spin, las tecnologías que usaremos serán:
Framework: Spring con Spring Boot 2 y Spring Cloud
Servidore de aplicaciones: Se usaran Saas en la nube usando AWS (S3, Cognito, EC2, CloudWatch, RDS, Lamdas, SQS) y Google Cloud
Kinesis
Base de datos: MongoDB, Redis
Kubernetes
Docker
Dynatrace
React Native
Configcat
Repositorio: Bitbucket usando git
Versión de Java: 8
Otros: OTP, retrofit
Spock Testing Framework
Adicional a esto se usara como IDE IntelliJ Community al tener una de las comunidades más activas y contar con plugins fáciles de instalar y usar.
Flujo de aseguramiento de la calidad del código
Con este flujo se pretende mostrar algunas herramientas que nos ayudaran a tener una calidad en el código adecuado al proyecto. Esto se muestra en esta imagen:
Diseño. Un desarrollo adecuado primero se codifica en papel y después en el IDE. Con esto en mente, debemos de analizar nuestra propuesta de solución con diagramas de alto y bajo nivel con el objetivo de identificar la solución más adecuada siguiendo los criterios ya descritos en este documento y aplicando en la solución algún patrón de desarrollo usando de forma correcta la herencia, polimorfismo, encapsulación, etc.
Se recomienda por lo menos tener un diagrama de clases y un diagrama de secuencia.
Codificación. Se realiza después de tener un análisis detallado de la solución, teniendo como insumo casos de uso o épicas que definan la funcionalidad que debe tener el proyecto y los criterios de aceptación de la solución. Adicional a esto se tendrán los diagramas de diseño.
Siguiendo las recomendaciones de este documento incrementaremos la calidad del código.
Pruebas Unitarias. Nos ayudan mayormente cuando el proyecto ya está avanzado y se requiere corregir o agregar funcionalidad, facilitando la validación de los cambios. Por esto es recomendable nunca omitirlas.
Cobertura. La cobertura de pruebas en el código mide el porcentaje de código que está cubierto por las pruebas unitarias, se recomendaría que por lo menos se tenga un 85% de cobertura del proyecto.
Se recomienda obtener reporte con Jacoco.
Sonar. Este manual expone muchas formas de contribuir a tener una buena calidad del código y para estar seguros una herramienta que nos apoya a medir la calidad es Sonar. Sonar nos apoya indicándonos los problemas más comunes que hay en el código, nos apoya con los issues de seguridad comunes, la cobertura de pruebas, etc. Para esto podemos alimentarlo con reglas y criterios que consideremos importantes, con el objetivo de obtener el máximo de esta herramienta.
Solo se aceptan code smells justificados, colocar justificación en el comentario de cada code smell.
Seguridad. Adicional a esto se realizara la validación de la seguridad con la ayuda de VERACODE.
Flujo de despliegue
Como parte importante del trabajo en equipo en un proyecto de desarrollo de software es la integración continua (CI) y la distribución continua (CD). Son dos factores importantes en los proyectos, uno define el cómo se integrara cada cambio de los desarrolladores y el otro es como se libera este desarrollo para el usuario, con este último se le genera valor al cliente.
Integración continua. Para esto podemos usar herramientas de control de versiones como SVN o GIT. En nuestro caso usaremos git, con github y bitbucket. La forma como iremos integrando nuestros cambios será la siguiente:
Commit inicial. Contendrá el proyecto base.
Commit Dev. Commmit con el que un desarrollador subirá sus cambios para integrarlos al código existente.
PullRequest. Solicitud de integración a la rama master, para realizar el merge el Tech Lead debe realizar el code review, en caso de que no haya hallazgos se integrara a la rama master. Si hay hallazgos se solicitara la atención de estos.
Commit Dev corrección de hallazgos. Sera el commit en el que se suben los cambios que resuelven los hallazgos identificados en el code review.
Merge. Se realiza la integración del código en la rama master
Snapshot. Es el despliegue en DEV.
Cherry Pick. Después de la aceptación de la liberación en DEV y que no se encuentren problemas, se integrara el código a la rama RC por medio de un Cherry Pick.
Distribución continua. Nos ayuda a realizar una liberación de forma constante y sencilla en uno o más ambientes, una de las herramientas más usadas es Jenkins con la ayuda de pipelines. Adicional a esto nos ayuda Docker y Kubernetes para poder gestionar instancias de forma dinámica de acuerdo a la demanda de los servicios quitándole un trabajo que antes era muy complejo a DevOps.
En nuestro caso DevOps se encargara de esta actividad.