Volver al blog

SOLID: ¿Los primeros 5 principios del diseño orientado a objetos?

SOLID: ¿Los primeros 5 principios del diseño orientado a objetos?

Introducción

SOLID es un acrónimo mnemotécnico para cinco principios de diseño orientado a objetos que fueron introducidos por Robert C. Martin, a quien popularmente se le conoce como Uncle Bob. Estos principios tienen como objetivo ayudar a los diseñadores, arquitectos, ingenieros y desarrolladores de software a crear software más flexible, mantenible y escalable. Al seguir estos principios, puede diseñar clases que sean más fáciles de probar, refactorizar, reutilizar y extender.

El acrónimo SOLID significa:

S – Principio de Responsabilidad Única

O – Principio de Abierto/Cerrado

L – Principio de Sustitución de Liskov

I – Principio de Segregación de Interfaces

D – Principio de Inversión de Dependencias

En este artículo, explicaremos cada principio individualmente para comprender cómo puede ayudarle a escribir mejor código. Además, agregaremos fragmentos de código para cada principio para mostrarle cómo puede aplicarlos en su camino de programación, así como lo que debe evitar en una arquitectura de código limpio. Para demostrar los conceptos, utilizaremos el lenguaje de programación Kotlin desarrollado por JetBrains y colaboradores de código abierto.

Principio de Responsabilidad Única

El Principio de Responsabilidad Única (SRP) es un principio de diseño de software que establece que cada clase o módulo en un programa debe tener una única responsabilidad bien definida. Esto significa que una clase debe tener solo una razón para cambiar, y debe ser responsable de una sola parte de la funcionalidad del programa.

Considere el siguiente fragmento de código como un ejemplo de cómo se puede aplicar este principio en Kotlin:

La palabra clave data en Kotlin indica que esta clase es una clase de datos, lo que significa que está diseñada para contener datos y no tiene ningún comportamiento complejo. Usando esta clase de datos, podemos crear una clase UserService  para administrar usuarios como se muestra a continuación:

En este ejemplo, la clase UserService tiene una única responsabilidad: administrar usuarios en una base de datos. Cada usuario está representado por el código de la data class User  compartido anteriormente. Todos los métodos de la clase UserService están relacionados con esta responsabilidad y, por lo tanto, son cohesivos. Esto hace que la clase sea más fácil de entender y mantener, ya que está claro que todos los métodos de la clase están relacionados con una única tarea bien definida.

Un método que viola el Principio de Responsabilidad Única (SRP) en la clase UserService sería uno que no esté relacionado con la responsabilidad principal de la clase, que es administrar usuarios en una base de datos. Por ejemplo, considere la siguiente variación de la clase UserService:

En este ejemplo, el sendEmail  método no se relaciona con la responsabilidad principal de la UserService clase, que es gestionar usuarios en una base de datos. Este método es responsable de enviar correos electrónicos, lo cual es una preocupación separada de la gestión de usuarios. Como resultado, este método viola el SRP, ya que introduce una segunda razón para que la clase UserService cambie.

Para cumplir con el Principio de Responsabilidad Única, sería mejor separar la funcionalidad de envío de correos electrónicos en una clase separada, como una clase EmailService . Esto permitiría que la clase UserService se concentre en su responsabilidad principal de gestionar usuarios, y la clase EmailService se concentre en su responsabilidad de enviar correos electrónicos.

Debe tener en cuenta que el Principio de Responsabilidad Única no se trata del número de métodos que tiene una clase, sino de la cohesión de los métodos y la clara separación de responsabilidades dentro de una clase.

Principio de Abierto/Cerrado

El Principio de Abierto/Cerrado (OCP) es un principio de diseño de software que establece que las entidades de software (como clases, módulos o funciones) deben estar abiertas para la extensión, pero cerradas para la modificación. Esto significa que debería ser posible añadir nueva funcionalidad a una clase o módulo sin cambiar su código existente.

Considere la clase UserService del ejemplo anterior. Supongamos que queremos añadir una nueva característica a la clase UserService que nos permita buscar usuarios por dirección de correo electrónico. Una forma de hacer esto sería añadir un nuevo método a la clase UserService como se destaca en el código a continuación:

Este enfoque funciona, pero viola el Principio de Abierto/Cerrado, ya que tuvimos que modificar la clase UserService existente para añadir la nueva característica. Un mejor enfoque sería utilizar la herencia o la composición para extender la funcionalidad de la clase UserService sin modificar su código.

Para lograr esto, podríamos crear una nueva clase llamada UserSearchService  que extienda la clase UserService y añada la funcionalidad de búsqueda por correo electrónico:

En este ejemplo, la clase UserSearchService está abierta para la extensión, ya que proporciona funcionalidad adicional más allá de lo que ofrece la clase UserService clase. Al mismo tiempo, la UserService clase permanece cerrada para su modificación, ya que no necesitamos modificar su código para agregar la función de búsqueda por correo electrónico.

Principio de sustitución de Liskov

El Principio de sustitución de Liskov (LSP) es un principio de diseño de software que establece que los objetos de una superclase deben poder ser reemplazados por objetos de una subclase sin afectar la corrección del programa. Esto significa que una subclase debe ser un sustituto válido para su superclase, y debe comportarse de la misma manera que la superclase cuando se utiliza en el mismo contexto.

Continuaremos la demostración utilizando la User y la UserService clases de los ejemplos anteriores. Para permitir que la clase User sea extendida, utilizamos la open  palabra clave en Kotlin:

Aquí está la clase original UserService que utiliza la clase de datos User de arriba:

Supongamos que queremos crear una subclase de User llamada AdminUser que represente a los usuarios con privilegios administrativos. Podríamos hacerlo de esta manera:

En este ejemplo, la clase AdminUser es un sustituto válido para la clase User, ya que se comporta de la misma manera que la clase User y se puede utilizar dondequiera que se espere un objeto User. Por ejemplo, podemos utilizar la clase AdminUser con la clase UserService  de esta manera:

Este código es correcto, ya que la clase AdminUser es un sustituto válido para la clase User y se puede utilizar de la misma manera que un objeto User.

Es importante tener en cuenta que el Principio de sustitución de Liskov es más que solo herencia. Se trata de garantizar que los objetos de una subclase se comporten de la misma manera que los objetos de la superclase, independientemente de cómo se implemente la subclase. Por ejemplo, si la clase AdminUser se comportara de manera diferente a la clase User en algún sentido, violaría el Principio de sustitución de Liskov, ya que no sería un sustituto válido para la clase User.

Principio de segregación de interfaces

El Principio de segregación de interfaces (ISP) es un principio de diseño de software que establece que los clientes no deben verse obligados a depender de interfaces que no utilizan. Esto significa que, por lo general, es una buena idea crear interfaces pequeñas y enfocadas que hagan bien una sola cosa, en lugar de crear grandes interfaces de propósito general que intenten hacer muchas cosas.

Aquí tiene un ejemplo de cómo se puede aplicar este principio reescribiendo las clases User  y UserService  de los ejemplos anteriores:

En este ejemplo, la UserService interfaz define cuatro métodos que están relacionados con la gestión de usuarios en una base de datos. La DatabaseUserService  clase implementa esta interfaz y proporciona implementaciones concretas para estos métodos.

Supongamos que queremos añadir una nueva función a la UserService interfaz que nos permita buscar usuarios por dirección de correo electrónico. Una forma de hacer esto sería añadir un nuevo método a la UserService interfaz:

Tu código no se ejecutará, a menos que también implementes este método en todas las clases que implementen la UserService interfaz:

Aunque este enfoque funciona, viola el Principio de Segregación de Interfaces, ya que obliga a la DatabaseUserService  clase a implementar un método ( searchUsersByEmail ) que tal vez no necesite o no utilice.

Un mejor enfoque sería crear una interfaz  separada para la funcionalidad de búsqueda por correo electrónico:

Ahora tenemos interfaces separadas, pequeñas y enfocadas, es decir, UserService  y UserSearchServiceque tienen una única responsabilidad. Una clase que requiera todas las funcionalidades de estas interfaces puede implementarlas como se muestra en el fragmento de código a continuación:

Esto cumple con el Principio de Segregación de Interfaces, ya que garantiza que los clientes (como la DatabaseUserService clase) no se vean obligados a depender de interfaces que no utilizan.

Para entender mejor este concepto, supongamos que tenemos otra clase llamada MemoryUserService  que implementa la UserService  interfaz pero no necesita la funcionalidad de búsqueda por correo electrónico, podemos escribir el código así:

In this example, the MemoryUserService clase solo necesita implementar los métodos definidos en la UserService interfaz, y no necesita preocuparse por la funcionalidad de búsqueda por correo electrónico. Esto permite que la MemoryUserService clase se concentre en su responsabilidad principal de gestionar usuarios en memoria, en lugar de verse obligada a implementar una funcionalidad no relacionada.

Principio de Inversión de Dependencias

El Principio de Inversión de Dependencias (DIP) es un principio de diseño de software que establece que los módulos de alto nivel no deben depender de los módulos de bajo nivel, sino que ambos deben depender de abstracciones. Esto significa que, por lo general, es una buena idea diseñar el software de tal manera que los componentes de alto nivel no estén vinculados a implementaciones específicas de componentes de bajo nivel, sino que dependan de abstracciones (como interfaces o clases abstractas) que puedan implementarse de diferentes maneras.

Veamos un ejemplo de cómo se puede aplicar este principio a las User y UserService clases utilizadas en los fragmentos de código anteriores:

En este ejemplo, la clase UserService depende de una abstracción llamada la interfaz UserRepository, en lugar de depender de una implementación específica de un repositorio de usuarios. Esto nos permite implementar la interfaz UserRepository de diferentes maneras, como con una base de datos o almacenamiento en memoria, sin afectar a la clase UserService.

Por ejemplo, aquí’s una implementación de la interfaz UserRepository que utiliza una base de datos:

Aquí hay otra implementación de la interfaz UserRepository que utiliza almacenamiento en memoria:

Esto hace que el sistema sea más flexible y mantenible, ya que nos permite cambiar la implementación del repositorio sin afectar al resto del sistema. También facilita la prueba de la clase UserService, ya que podemos simular la dependencia UserRepository en nuestras pruebas.

Conclusión

En este artículo, hablamos sobre los cinco principios de SOLID  código y fragmentos de código compartidos que cumplen con cada principio. Adherirse a estos principios puede ayudarte a diseñar sistemas de software que sean más flexibles, mantenibles y escalables. Sin embargo, es importante tener en cuenta que estos principios son pautas, en lugar de reglas estrictas, y depende del desarrollador decidir cuándo y cómo aplicarlos en el contexto de su proyecto específico. Continúa tu aprendizaje visitando nuestro blog para leer artículos más detallados y actualizados y tutoriales sobre computación en la nube y DevOps, diseño y desarrollo de software, tendencias tecnológicas a tener en cuenta y más.

¡Feliz programación!

author

Preslav Dobrev

Autor · CloudSigma

Preslav Dobrev es diseñador creativo en CloudSigma, centrado en una identidad empresarial coherente mediante el uso de canales de marketing tradicionales e innovadores. Es experto en fusionar la visión artística con el marketing estratégico para crear narrativas de marca impactantes.

Comentarios

Aún no hay comentarios. Sea el primero.