CodeGym /Blog Java /Random-ES /Reglas de codificación: desde la creación de un sistema h...
John Squirrels
Nivel 41
San Francisco

Reglas de codificación: desde la creación de un sistema hasta el trabajo con objetos

Publicado en el grupo Random-ES
¡Buen dia a todos! Hoy nos gustaría hablarte sobre cómo escribir un buen código. Por supuesto, no todos quieren masticar libros como Clean Code de inmediato, ya que contienen una gran cantidad de información, pero no mucho está claro al principio. Y para cuando termine de leer, puede matar todo su deseo de codificar. Teniendo en cuenta todo eso, hoy quiero brindarles una pequeña guía (un pequeño conjunto de recomendaciones) para escribir un mejor código. En este artículo, repasemos las reglas y los conceptos básicos relacionados con la creación de un sistema y el trabajo con interfaces, clases y objetos. Leer este artículo no te llevará mucho tiempo y, espero, no te aburrirá. Me abriré camino de arriba hacia abajo, es decir, desde la estructura general de una aplicación hasta sus detalles más específicos. Reglas de codificación: desde la creación de un sistema hasta el trabajo con objetos - 1

Sistemas

Las siguientes son características generalmente deseables de un sistema:
  • Mínima complejidad. Deben evitarse los proyectos demasiado complicados. Lo más importante es la sencillez y la claridad (más simple = mejor).
  • Facilidad de mantenimiento. Al crear una aplicación, debe recordar que necesitará mantenimiento (incluso si usted personalmente no será responsable de mantenerlo). Esto significa que el código debe ser claro y obvio.
  • Bajo acoplamiento. Esto significa que minimizamos el número de dependencias entre las diferentes partes del programa (maximizando nuestro cumplimiento con los principios de programación orientada a objetos).
  • Reutilización. Diseñamos nuestro sistema con la capacidad de reutilizar componentes en otras aplicaciones.
  • Portabilidad. Debería ser fácil adaptar un sistema a otro entorno.
  • Estilo uniforme. Diseñamos nuestro sistema utilizando un estilo uniforme en sus diversos componentes.
  • Extensibilidad (escalabilidad). Podemos mejorar el sistema sin violar su estructura básica (agregar o cambiar un componente no debería afectar a todos los demás).
Es prácticamente imposible construir una aplicación que no requiera modificaciones o nuevas funcionalidades. Constantemente necesitaremos agregar nuevas piezas para ayudar a nuestra creación a mantenerse al día. Aquí es donde entra en juego la escalabilidad. La escalabilidad consiste esencialmente en extender la aplicación, agregar nuevas funcionalidades y trabajar con más recursos (o, en otras palabras, con una mayor carga). En otras palabras, para que sea más fácil agregar nueva lógica, nos ceñimos a algunas reglas, como reducir el acoplamiento del sistema aumentando la modularidad.Reglas de codificación: desde la creación de un sistema hasta el trabajo con objetos - 2

Fuente de imagen

Etapas del diseño de un sistema.

  1. Sistema de software. Diseñe la aplicación en general.
  2. División en subsistemas/paquetes. Defina partes lógicamente distintas y defina las reglas para la interacción entre ellas.
  3. División de subsistemas en clases. Dividir partes del sistema en clases e interfaces específicas y definir la interacción entre ellas.
  4. División de clases en métodos. Cree una definición completa de los métodos necesarios para una clase, en función de su responsabilidad asignada.
  5. Diseño de métodos. Cree una definición detallada de la funcionalidad de los métodos individuales.
Por lo general, los desarrolladores ordinarios manejan este diseño, mientras que el arquitecto de la aplicación maneja los puntos descritos anteriormente.

Principios y conceptos generales del diseño de sistemas.

Inicialización perezosa. En este idioma de programación, la aplicación no pierde el tiempo creando un objeto hasta que realmente se usa. Esto acelera el proceso de inicialización y reduce la carga en el recolector de elementos no utilizados. Dicho esto, no deberías llevar esto demasiado lejos, porque eso puede violar el principio de modularidad. Quizás valga la pena mover todas las instancias de construcción a alguna parte específica, por ejemplo, el método principal o una clase de fábrica . Una característica del buen código es la ausencia de código repetitivo y repetitivo. Como regla general, dicho código se coloca en una clase separada para que pueda llamarse cuando sea necesario.

POA

También me gustaría señalar la programación orientada a aspectos. Este paradigma de programación tiene que ver con la introducción de una lógica transparente. Es decir, el código repetitivo se coloca en clases (aspectos) y se llama cuando se cumplen ciertas condiciones. Por ejemplo, al llamar a un método con un nombre específico o acceder a una variable de un tipo específico. A veces, los aspectos pueden ser confusos, ya que no está claro de inmediato desde dónde se llama al código, pero esta es una funcionalidad muy útil. Especialmente al almacenar en caché o iniciar sesión. Agregamos esta funcionalidad sin agregar lógica adicional a las clases ordinarias. Las cuatro reglas de Kent Beck para una arquitectura simple:
  1. Expresividad: la intención de una clase debe expresarse claramente. Esto se logra a través de una denominación adecuada, un tamaño pequeño y la adhesión al principio de responsabilidad única (que consideraremos con más detalle a continuación).
  2. Número mínimo de clases y métodos: en su deseo de hacer que las clases sean lo más pequeñas y enfocadas posible, puede ir demasiado lejos (lo que resulta en el anti-patrón de cirugía de escopeta). Este principio exige mantener el sistema compacto y no ir demasiado lejos, creando una clase separada para cada acción posible.
  3. Sin duplicación: el código duplicado, que crea confusión y es una indicación de un diseño de sistema subóptimo, se extrae y se mueve a una ubicación separada.
  4. Ejecuta todas las pruebas: un sistema que pasa todas las pruebas es manejable. Cualquier cambio podría hacer que una prueba falle, lo que nos revela que nuestro cambio en la lógica interna de un método también cambió el comportamiento del sistema de formas inesperadas.

SÓLIDO

Al diseñar un sistema, vale la pena considerar los conocidos principios SOLID:

S (responsabilidad única), O (abierto-cerrado), L (sustitución de Liskov), I (segregación de interfaz), D (inversión de dependencia).

No nos detendremos en cada principio individual. Eso estaría un poco más allá del alcance de este artículo, pero puedes leer más aquí .

Interfaz

Quizás uno de los pasos más importantes para crear una clase bien diseñada es crear una interfaz bien diseñada que represente una buena abstracción, ocultando los detalles de implementación de la clase y presentando simultáneamente un grupo de métodos que son claramente consistentes entre sí. Echemos un vistazo más de cerca a uno de los principios SOLID: segregación de interfaz: los clientes (clases) no deben implementar métodos innecesarios que no usarán. En otras palabras, si estamos hablando de crear una interfaz con la menor cantidad de métodos destinados a realizar el único trabajo de la interfaz (que creo que es muy similar al principio de responsabilidad única), es mejor crear un par de métodos más pequeños. de una interfaz hinchada. Afortunadamente, una clase puede implementar más de una interfaz. Recuerde nombrar sus interfaces correctamente: el nombre debe reflejar la tarea asignada con la mayor precisión posible. Y, por supuesto, cuanto más corto sea, menos confusión causará. Los comentarios de la documentación generalmente se escriben en el nivel de la interfaz. Estos comentarios proporcionan detalles sobre lo que debe hacer cada método, qué argumentos toma y qué devolverá.

Clase

Reglas de codificación: desde la creación de un sistema hasta el trabajo con objetos - 3

Fuente de imagen

Echemos un vistazo a cómo se organizan las clases internamente. O mejor dicho, algunas perspectivas y reglas que se deben seguir al escribir clases. Como regla general, una clase debe comenzar con una lista de variables en un orden específico:
  1. constantes estáticas públicas;
  2. constantes estáticas privadas;
  3. variables de instancia privada.
Luego vienen los diversos constructores, en orden desde los que tienen menos argumentos hasta los que tienen más. Les siguen métodos desde los más públicos hasta los más privados. En términos generales, los métodos privados que ocultan la implementación de alguna funcionalidad que queremos restringir se encuentran en la parte inferior.

Tamaño de la clase

Ahora me gustaría hablar sobre el tamaño de las clases. Recordemos uno de los principios SOLID: el principio de responsabilidad única. Establece que cada objeto tiene un solo propósito (responsabilidad), y la lógica de todos sus métodos apunta a lograrlo. Esto nos dice que evitemos clases grandes e infladas (que en realidad son el antipatrón del objeto de Dios), y si tenemos muchos métodos con todo tipo de lógica diferente metidos en una clase, debemos pensar en dividirla en una clase. par de partes lógicas (clases). Esto, a su vez, aumentará la legibilidad del código, ya que no llevará mucho tiempo comprender el propósito de cada método si conocemos el propósito aproximado de cualquier clase dada. Además, vigile el nombre de la clase, que debe reflejar la lógica que contiene. Por ejemplo, si tenemos una clase con más de 20 palabras en su nombre, tenemos que pensar en la refactorización. Cualquier clase que se precie no debería tener tantas variables internas. De hecho, cada método funciona con uno o algunos de ellos, provocando mucha cohesión dentro de la clase (que es exactamente como debería ser, ya que la clase debería ser un todo unificado). Como resultado, aumentar la cohesión de una clase conduce a una reducción del tamaño de la clase y, por supuesto, aumenta el número de clases. Esto es molesto para algunas personas, ya que necesita examinar más los archivos de clase para ver cómo funciona una tarea grande específica. Además de todo, cada clase es un pequeño módulo que debe estar mínimamente relacionado con los demás. Este aislamiento reduce la cantidad de cambios que debemos realizar al agregar lógica adicional a una clase. cada método trabaja con uno o algunos de ellos, causando mucha cohesión dentro de la clase (que es exactamente como debería ser, ya que la clase debería ser un todo unificado). Como resultado, aumentar la cohesión de una clase conduce a una reducción del tamaño de la clase y, por supuesto, aumenta el número de clases. Esto es molesto para algunas personas, ya que necesita examinar más los archivos de clase para ver cómo funciona una tarea grande específica. Además de todo, cada clase es un pequeño módulo que debe estar mínimamente relacionado con los demás. Este aislamiento reduce la cantidad de cambios que debemos realizar al agregar lógica adicional a una clase. cada método trabaja con uno o algunos de ellos, causando mucha cohesión dentro de la clase (que es exactamente como debería ser, ya que la clase debería ser un todo unificado). Como resultado, aumentar la cohesión de una clase conduce a una reducción del tamaño de la clase y, por supuesto, aumenta el número de clases. Esto es molesto para algunas personas, ya que necesita examinar más los archivos de clase para ver cómo funciona una tarea grande específica. Además de todo, cada clase es un pequeño módulo que debe estar mínimamente relacionado con los demás. Este aislamiento reduce la cantidad de cambios que debemos realizar al agregar lógica adicional a una clase. Esta cohesión conduce a una reducción del tamaño de las clases y, por supuesto, aumenta el número de clases. Esto es molesto para algunas personas, ya que necesita examinar más los archivos de clase para ver cómo funciona una tarea grande específica. Además de todo, cada clase es un pequeño módulo que debe estar mínimamente relacionado con los demás. Este aislamiento reduce la cantidad de cambios que debemos realizar al agregar lógica adicional a una clase. Esta cohesión conduce a una reducción del tamaño de las clases y, por supuesto, aumenta el número de clases. Esto es molesto para algunas personas, ya que necesita examinar más los archivos de clase para ver cómo funciona una tarea grande específica. Además de todo, cada clase es un pequeño módulo que debe estar mínimamente relacionado con los demás. Este aislamiento reduce la cantidad de cambios que debemos realizar al agregar lógica adicional a una clase.

Objetos

Encapsulación

Aquí hablaremos primero sobre un principio de programación orientada a objetos: encapsulación. Ocultar la implementación no equivale a crear un método para aislar variables (restringir el acceso sin pensar a través de métodos individuales, getters y setters, lo cual no es bueno, ya que se pierde todo el punto de encapsulación). Ocultar el acceso tiene como objetivo formar abstracciones, es decir, la clase proporciona métodos concretos compartidos que usamos para trabajar con nuestros datos. Y el usuario no necesita saber exactamente cómo estamos trabajando con estos datos, funciona y eso es suficiente.

Ley de Deméter

También podemos considerar la Ley de Deméter: es un pequeño conjunto de reglas que ayuda a gestionar la complejidad a nivel de clase y método. Supongamos que tenemos un objeto Car y tiene un método move(Object arg1, Object arg2) . Según la Ley de Deméter, este método se limita a llamar:
  • métodos del propio objeto Coche (en otras palabras, el objeto "este");
  • métodos de objetos creados dentro del método de movimiento ;
  • métodos de objetos pasados ​​como argumentos ( arg1 , arg2 );
  • métodos de objetos Car internos (nuevamente, "esto").
En otras palabras, la Ley de Deméter es algo así como lo que los padres le dirían a un niño: "puedes hablar con tus amigos, pero no con extraños".

Estructura de datos

Una estructura de datos es una colección de elementos relacionados. Al considerar un objeto como una estructura de datos, hay un conjunto de elementos de datos sobre los que operan los métodos. La existencia de estos métodos se asume implícitamente. Es decir, una estructura de datos es un objeto cuyo propósito es almacenar y trabajar con (procesar) los datos almacenados. Su principal diferencia con un objeto regular es que un objeto ordinario es una colección de métodos que operan en elementos de datos que implícitamente se supone que existen. ¿Lo entiendes? El aspecto principal de un objeto ordinario son los métodos. Las variables internas facilitan su correcto funcionamiento. Pero en una estructura de datos, los métodos están ahí para respaldar su trabajo con los elementos de datos almacenados, que son primordiales aquí. Un tipo de estructura de datos es un objeto de transferencia de datos (DTO). Esta es una clase con variables públicas y sin métodos (o solo métodos para lectura/escritura) que se usa para transferir datos cuando se trabaja con bases de datos, analizando mensajes de sockets, etc. Los datos generalmente no se almacenan en tales objetos por un período prolongado. Se convierte casi inmediatamente al tipo de entidad con la que trabaja nuestra aplicación. Una entidad, a su vez, también es una estructura de datos, pero su propósito es participar en la lógica comercial en varios niveles de la aplicación. El propósito de un DTO es transportar datos hacia/desde la aplicación. Ejemplo de DTO: es también una estructura de datos, pero su propósito es participar en la lógica empresarial en varios niveles de la aplicación. El propósito de un DTO es transportar datos hacia/desde la aplicación. Ejemplo de DTO: es también una estructura de datos, pero su propósito es participar en la lógica empresarial en varios niveles de la aplicación. El propósito de un DTO es transportar datos hacia/desde la aplicación. Ejemplo de DTO:

@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
Todo parece bastante claro, pero aquí nos enteramos de la existencia de los híbridos. Los híbridos son objetos que tienen métodos para manejar lógica importante, almacenar elementos internos y también incluyen métodos de acceso (get/set). Dichos objetos son desordenados y dificultan la adición de nuevos métodos. Debe evitarlos, porque no está claro para qué sirven: ¿almacenar elementos o ejecutar lógica?

Principios de creación de variables

Reflexionemos un poco sobre las variables. Más específicamente, pensemos qué principios se aplican al crearlos:
  1. Idealmente, debe declarar e inicializar una variable justo antes de usarla (no cree una y se olvide de ella).
  2. Siempre que sea posible, declare las variables como finales para evitar que su valor cambie después de la inicialización.
  3. No se olvide de las variables de contador, que generalmente usamos en algún tipo de ciclo for . Es decir, no olvide ponerlos a cero. De lo contrario, toda nuestra lógica puede romperse.
  4. Debe intentar inicializar las variables en el constructor.
  5. Si hay una opción entre usar un objeto con una referencia o sin ( nuevo SomeObject() ), opte por sin, ya que después de que se use el objeto, se eliminará durante el próximo ciclo de recolección de elementos no utilizados y sus recursos no se desperdiciarán.
  6. Mantenga la vida útil de una variable (la distancia entre la creación de la variable y la última vez que se hace referencia a ella) lo más corta posible.
  7. Inicialice las variables utilizadas en un ciclo justo antes del ciclo, no al comienzo del método que contiene el ciclo.
  8. Comience siempre con el alcance más limitado y amplíe solo cuando sea necesario (debe intentar que una variable sea lo más local posible).
  9. Use cada variable para un solo propósito.
  10. Evite las variables con un propósito oculto, por ejemplo, una variable dividida entre dos tareas; esto significa que su tipo no es adecuado para resolver una de ellas.

Métodos

Reglas de codificación: desde la creación de un sistema hasta el trabajo con objetos - 4

de la película "Star Wars: Episodio III - La venganza de los Sith" (2005)

Pasemos directamente a la implementación de nuestra lógica, es decir, a los métodos.
  1. Regla #1 — Compacidad. Idealmente, un método no debe exceder las 20 líneas. Esto significa que si un método público "aumenta" significativamente, debe pensar en separar la lógica y moverla a métodos privados separados.

  2. Regla n.º 2: if , else , while y otras declaraciones no deben tener bloques muy anidados: muchos anidamientos reducen significativamente la legibilidad del código. Idealmente, no debería tener más de dos bloques {} anidados .

    Y también es deseable mantener el código en estos bloques compacto y simple.

  3. Regla n.º 3: un método debe realizar solo una operación. Es decir, si un método realiza todo tipo de lógica compleja, lo dividimos en submétodos. Como resultado, el método en sí será una fachada cuyo propósito es llamar a todas las demás operaciones en el orden correcto.

    Pero, ¿qué pasa si la operación parece demasiado simple para ponerla en un método separado? Cierto, a veces puede parecer como disparar un cañón a los gorriones, pero los métodos pequeños brindan una serie de ventajas:

    • Mejor comprensión del código;
    • Los métodos tienden a volverse más complejos a medida que avanza el desarrollo. Si un método es simple para empezar, será un poco más fácil complicar su funcionalidad;
    • Los detalles de implementación están ocultos;
    • Reutilización de código más fácil;
    • Código más fiable.

  4. La regla de reducción: el código debe leerse de arriba a abajo: cuanto más bajo lea, más profundizará en la lógica. Y viceversa, cuanto más asciendes, más abstractos son los métodos. Por ejemplo, las declaraciones de cambio son poco compactas e indeseables, pero si no puede evitar usar un cambio, debe intentar moverlo lo más bajo posible, a los métodos de nivel más bajo.

  5. Argumentos del método — ¿Cuál es el número ideal? Idealmente, ninguno en absoluto :) ¿Pero eso realmente sucede? Dicho esto, debe tratar de tener la menor cantidad de argumentos posible, porque cuantos menos haya, más fácil será usar un método y más fácil será probarlo. En caso de duda, trate de anticipar todos los escenarios para usar el método con una gran cantidad de parámetros de entrada.

  6. Además, sería bueno separar los métodos que tienen un indicador booleano como parámetro de entrada, ya que esto por sí solo implica que el método realiza más de una operación (si es verdadero, entonces haga una cosa; si es falso, entonces haga otra). Como escribí anteriormente, esto no es bueno y debe evitarse si es posible.

  7. Si un método tiene una gran cantidad de parámetros de entrada (un extremo es 7, pero realmente debería comenzar a pensar después de 2 o 3), algunos de los argumentos deben agruparse en un objeto separado.

  8. Si hay varios métodos similares (sobrecargados), los parámetros similares deben pasarse en el mismo orden: esto mejora la legibilidad y la usabilidad.

  9. Cuando pasa parámetros a un método, debe estar seguro de que se usan todos, de lo contrario, ¿para qué los necesita? Elimine cualquier parámetro no utilizado de la interfaz y termine con eso.

  10. try/catch no se ve muy bien en la naturaleza, por lo que sería una buena idea moverlo a un método intermedio separado (un método para manejar excepciones):

    
    public void exceptionHandling(SomeObject obj) {
        try {  
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

Hablé sobre el código duplicado anteriormente, pero permítanme repetirlo una vez más: si tenemos un par de métodos con código repetido, debemos moverlo a un método separado. Esto hará que tanto el método como la clase sean más compactos. No se olvide de las reglas que rigen los nombres: los detalles sobre cómo nombrar correctamente clases, interfaces, métodos y variables se discutirán en la siguiente parte del artículo. Pero eso es todo lo que tengo para ti hoy.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION