Criterios de diseño son pautas lo que nos ayuda a mejorar la calidad de nuestra aplicación. Son las soluciones preferidas para problemas que ocurren comúnmente y que, cuando no se tratan adecuadamente, conducen a un mal diseño.
Discutiremos algunos de los principios de diseño para la programación orientada a objetos que todo desarrollador debería conocer. Estos principios de diseño van más allá de los conceptos centrales orientados a objetos como herencia, encapsulación, abstracción y polimorfismo.
En este artículo, analizaré algunos de los principios de diseño fundamentales que utilicé en mi viaje de desarrollo web para resolver algunos problemas comunes.
“Encapsular lo que varía”“Composición de favores”“Programa para interfaces”“Bajo acoplamiento”Principios SÓLIDOS
Criterios de diseño son generales pautas que pueden guiar la estructura y las relaciones de su clase. Por otro lado, Patrones de diseño están probados soluciones que resuelven problemas comúnmente recurrentes. Dicho esto, la mayoría de las implementaciones prácticas de estos principios de diseño se realizan principalmente utilizando uno o más patrones de diseño.
Considerado como los principios fundamentales del diseño, este principio se encuentra en funcionamiento en casi todos los patrones de diseño.
Este principio sugiere, Identifique los aspectos de sus aplicaciones que varían y sepárelos de lo que permanece igual. Si un componente o módulo de su aplicación va a cambiar con frecuencia, entonces es una buena práctica separar esta parte del código de las estables para que luego podamos ampliar o alterar la parte que varía sin afectar las que no varían.
La mayoría de los patrones de diseño como Estrategia, Adaptador, Fachada, Decorador, Observador, etc. siguen este principio.
Por ejemplo, supongamos que estamos diseñando una aplicación para una empresa que brinda servicios en línea de electrodomésticos. En nuestro core de aplicaciones tenemos este método. procesoSolicitud de Servicio()cuyo propósito es crear una instancia de un Servicio en línea clase basada en el Tipo de servicio y procesar la solicitud. Este es un método de trabajo pesado como se muestra a continuación.
público vacío ProcessServiceRequest (tipo de servicio de cadena) { Servicio de servicio en línea;
si(servicio.equals (“Aire acondicionado”)) servicio = nuevo Servicio AC(); demás si(servicio.equals(“Lavadora”)) servicio = nuevo WMServicio(); demás si(servicio.equals(“Refrigerador”)) servicio = nuevo RFServicio(); demás servicio = nuevo Servicio general(); servicio.getinfo(); servicio.assignServiceRequest(); servicio.assignAgent(); servicio.processRequest();}
En este caso, el tipo de servicio es una funcionalidad que seguramente cambiará en cualquier momento. Podríamos eliminar algunos servicios o agregar nuevos y cada cambio en las implementaciones requeriría cambiar este fragmento de código.
Entonces, según las pautas de “Encapsular lo que varía” Necesitamos encontrar el código que seguramente variará y separarlo para que cualquier error en el mismo no afecte la parte importante del código.
Podemos eliminar el código que crea instancias y crear una clase que funcione como una clase de fábrica que solo está ahí para proporcionar el tipo de instancias requerido. vamos a seguir el Patrón de diseño de fábrica aquí y tendremos solo un método obtenerServicioEnLínea() en esta clase que haría nuestro trabajo
público clase Fábrica de servicios en línea { público OnlineService getOnlineService (tipo de cadena) { Servicio de servicio en línea; si(servicio.equals (“Aire acondicionado”)) servicio = nuevo Servicio AC(); demás si(servicio.equals(“Lavadora”)) servicio = nuevo WMServicio(); demás si(servicio.equals(“Refrigerador”)) servicio = nuevo RFServicio(); demás servicio = nuevo Servicio general(); devolver servicio; }}
ahora podemos refactorizar nuestro código anterior como,
público vacío ProcessServiceRequest (tipo de servicio de cadena) { Servicio de servicio en línea = nuevo OnlineServiceFactory().getOnlineService(tipo de servicio); servicio.getinfo(); servicio.assignServiceRequest(); servicio.assignAgent(); servicio.processRequest();}
Ahora cualquier cambio en los tipos de servicio no afectará al resto del código.
La programación orientada a objetos proporciona 2 tipos de relaciones entre clases y sus instancias. tiene-a (composición) y es-a (herencia). Este principio de diseño esencialmente nos sugiere que “tiene un Se debe preferir la relación a es un relación”.
La mayoría de los desarrolladores, incluido yo, tendemos a inclinarnos por la herencia como primer recurso en la mayoría de los casos para evitar la duplicación del código y mantener la reutilización. Aunque es una buena práctica, a veces la herencia, cuando se usa en exceso, hace que nuestro código sea más rígido y no extensible.
Tomemos el ejemplo del caso anterior, hemos utilizado es un relación para gestionar la clase OnlineService con clases de implementación de servicios heredados como Servicio AC, Servicio WM, Servicio RF, etc.. Considere que la empresa decide expandir su negocio masivamente agregando más servicios como Servicio de bicicletas, Servicio de automóviles, Servicio de TV, Servicio de computadoras portátiles y muchos otros servicios domésticos. La implementación anterior seguiría siendo válida con el modelo de herencia. Pero la parte complicada aquí es que ahora han planeado incluir múltiples modelos de servicio juntos, como por ejemplo una solicitud para que se reparen múltiples electrodomésticos de cocina junto con el aire acondicionado. Esto llevaría a la creación de nuevas clases para combinaciones separadas como ACAndFridgeService, ACAndTVServices. Lo cual no es factible y es literalmente un mal diseño. Y cuanto más la combinación más vulnerable sería este diseño a romperse.
Por lo tanto, querríamos ir con un tiene un implementación de la relación. Donde todos y cada uno de los servicios en sí mismos son una sola clase y, según la solicitud, podríamos combinarlos y crear nuestra solicitud final.
podríamos usar Constructor patrón de diseño aquí y devolver un objeto compuesto con cada servicio solicitado por el usuario.
El acoplamiento flojo es un principio que sugiere que “Los componentes que interactúan entre sí deben ser independientes entre sí, confiando lo menos posible en el conocimiento de otros componentes..
Este es un principio que seguimos en una arquitectura de microservicio. Los servicios que interactúan entre sí son independientes entre sí. La interacción se basa estrictamente en los contratos de datos.
La implementación del acoplamiento flexible variará de un escenario a otro según el problema que intentamos resolver. Pero para nuestro ejemplo tomemos un escenario simple en tiempo real.
Imaginemos que estamos diseñando una aplicación frontend para una arquitectura no monolítica. Tenemos un formulario para registrarse que obtendría la entrada del usuario, realizaría validaciones del lado del cliente, enviaría una solicitud al servidor a través de API de descanso, obtendría la respuesta y, en función de la respuesta, decidiría mostrar mensajes de éxito o fracaso al usuario.
Aquí, podemos dividir la funcionalidad en dos según su propiedad. Idealmente, todo lo específico del lado del cliente debería estar desacoplado de las operaciones del lado del servidor. Es decir, el fragmento de código responsable de la visualización del formulario, las validaciones del lado del cliente y la visualización de mensajes al usuario no debe tener nada que ver con la parte del código que realiza solicitudes de API, procesa la respuesta y el estado de devolución.
Este principio de diseño nos guía a utilizar tipos abstractos, no concretos. “Programa para interfaces” en realidad significa programa para un supertipo como una interfaz o clase abstracta en Java. Estamos implementando polimorfismo programando en un supertipo para que el objeto de tiempo de ejecución real no esté bloqueado en su código.
Suponga una capa de acceso a la base de datos en su aplicación que se utiliza para realizar operaciones CRUD en su base de datos. Consideremos que implementamos un Servicio clase que llama al Cliente de base de datos clase (Sin embargo, en la práctica deberíamos tener una Accesor de datos clase entre Servicio y Cliente de base de datos). El Cliente de base de datos es una clase concreta programada para acceder a la base de datos postgres. El Cliente de base de datos es una clase de alta resistencia con todos los métodos auxiliares necesarios para acceder a la base de datos. Supongamos que el cliente decide cambiar a una base de datos NoSQL como MongoDB o agregarla como base de datos secundaria para algunos propósitos específicos. Esto llevaría a reescribir el Cliente de base de datos lo que complicaría las cosas.
¿Solución? Como establece este principio, cualquiera de estos módulos debe tener un supertipo abstracto como una interfaz. Los métodos básicos deberían estar disponibles en la interfaz. Se deben implementar implementaciones específicas para implementar la interfaz.
Servicio ServiceClass = nueva ServiceClass();if(tipoBD == “POSTGRES”) { service.db = nuevo PostgresDBClient();} más si(dbType == “MYSQL”) { service.db = nuevo MySQLDBClient();}
SÓLIDO es un conjunto de 5 principios que forman el acrónimo.
a.) Principio de responsabilidad única:
Una clase debe tener solo una razón para cambiar. Este principio sugiere que las responsabilidades de una clase deben ser limitadas. No es una directriz clara porque no tenemos una instrucción específica sobre la cual juzgar esto. Sin embargo, la idea básica al diseñar utilizando este principio debería ser comprobar si los métodos dentro de una clase son cohesivos, es decir, ¿realmente necesitan estar juntos?
Por ejemplo, una clase que debe crear un rectángulo que obtiene el área y la dibuja en la pantalla.
Esta implementación viola el principio de responsabilidad única, ya que calcular el área y dibujar son acciones separadas y pueden desacoplarse.
b.) Principio abierto cerrado:
Este principio establece que nuestra El diseño debe estar abierto a ampliaciones pero cerrado a modificaciones.. Pero ¿qué pasa si queremos agregar una nueva implementación en nuestro diseño? Podemos abordar esto de 2 maneras.
Extendemos la funcionalidad existente a una nueva clase y agregamos las implementaciones allí. Usamos composición para aceptar nuevos comportamientos.
Imaginemos que tenemos una clase para audicionar cantantes.
Cantantes de clase pública { Nombre de cadena;
edad interna;
Clasificaciones de cadenas;
Tipo de cadenaOfSinging;}
Sin embargo, si decidimos ampliar el alcance del concurso también al baile, entonces no podremos tener esta implementación concreta. Lo que podríamos hacer es, de acuerdo con este principio, abstraer y extender nuestra implementación a continuación.
Concursantes de clase abstracta pública { Nombre de cadena;
edad interna;
Clasificaciones de cadenas;}
clase pública Cantante implementa Concursantes { Nombre de cadena;
edad interna;
Clasificaciones de cadenas;
Tipo de cadenaDeMúsica; canto público vacío() {
…
}}clase pública Bailarina implementa Concursantes { Nombre de cadena;
edad interna;
Clasificaciones de cadenas;
Tipo de cadenaOfDance; baile público vacío () {
…
}}
Al introducir la herencia, resolvemos la complejidad anterior usando Principio abierto cerrado.
c.) Principio de sustitución de Liskov:
A veces, la herencia puede dañar el sistema cuando sustituimos nuestra clase derivada por la clase principal. El principio de Liskov esencialmente sugiere que, “Siempre deberías poder sustituir subtipos por su clase base”.
Por ejemplo, si tenemos un Clase infantil que extiende un clase de padres, entonces nuestro sistema no debería fallar cuando nuestra instancia de tipo principal se sustituye por una clase secundaria.
ClaseA claseA = nueva ClaseA();
claseA.hacerAlgo(); // debería funcionar bienClaseA = nueva ClaseB();
claseA.hacerAlgo(); // debería funcionar bien
d.) Principio de segregación de interfaces:
Este principio sugiere que un “La interfaz siempre debe ser coherente”.. Es decir, los componentes de la interfaz deben ser altamente identificables. Cualquier componente que no esté relacionado entre sí debe separarse y segregarse.
e.) Principio de inversión de dependencia:
El principio de inversión de dependencia se utiliza para desacoplar módulos de software. Se afirma que “Los módulos de alto nivel no deberían depender de los módulos de bajo nivel. En cambio, ambos deberían depender de la abstracción”.
Por ejemplo, considere un notificador clase que envía notificaciones por SMS desde Mensajero SMS clase como se muestra a continuación.
Ahora supongamos que queremos agregar 2 modos de notificación más: correo electrónico y WhatsApp. desde el
