






















Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity
Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium
Prepara tus exámenes
Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity
Prepara tus exámenes con los documentos que comparten otros estudiantes como tú en Docsity
Encuentra los documentos específicos para los exámenes de tu universidad
Estudia con lecciones y exámenes resueltos basados en los programas académicos de las mejores universidades
Responde a preguntas de exámenes reales y pon a prueba tu preparación
Consigue puntos base para descargar
Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium
Comunidad
Pide ayuda a la comunidad y resuelve tus dudas de estudio
Ebooks gratuitos
Descarga nuestras guías gratuitas sobre técnicas de estudio, métodos para controlar la ansiedad y consejos para la tesis preparadas por los tutores de Docsity
resumen de temas vistos durante el tiempo de clases
Tipo: Apuntes
1 / 30
Esta página no es visible en la vista previa
¡No te pierdas las partes importantes!























Antonio Leiva
Principios SOLID
© 2016 - 2021 Antonio Leiva
Los Principios SOLID son uno de los conceptos de programación y diseño de software más populares.
Seguramente ya has intentado muchas veces aprenderlos y aplicarlos en tu día a día, pero se te siguen resistiendo.
Esto le pasa a todo el mundo, porque son conceptos abstractos difíciles de explicar y no se dan los detalles necesarios para bajarlos a tierra.
Soy Antonio Leiva, ingeniero de software y formador, y tras enseñarles a muchos de mis alumnos este principios, puedo darte las claves para que los entiendas y los apliques de una vez por todas.
Son un conjunto de principios aplicables a la Programación Orientada a Objetos que, si los usas correctamente, te ayudarán a escribir software de calidad en cualquier lenguaje de programación orientada a objetos. Gracias a ellos, crearás código que será más fácil de leer, testear y mantener.
Los principios en los que se basa SOLID son los siguientes:
Estos principios son la base de mucha literatura que encontrarás en torno al desarrollo de software: muchas arquitecturas se basan en ellos para proveer flexibilidad, el testing necesita confiar en ellos para poder validar partes de código de forma independiente, y los procesos de refactorización serán mucho más sencillos si se cumplen estas reglas. Así que es muy conveniente que asimiles bien estos conceptos.
Fueron publicados por primera vez por Robert C. Martin¹, también conocido como Uncle Bob, en su libro Agile Software Development: Principles, Patterns, and Practices². Una persona que te recomiendo seguir, y echarle un vistazo a su blog³ de vez en cuando.
Las ventajas de utilizar los Principios SOLID son innumerables, ya que nos aportan todas esas características que siempre queremos ver en un software de calidad.
En cada uno de los principios nos iremos centrando en qué aportan específicamente, pero es interesante hacer un resumen general de lo que conseguiremos con ellos:
Los conceptos de cohesión y acoplamiento merecen un artículo a parte, pero a grandes rasgos lo que buscamos de un buen código es que sus clases puedan trabajar de forma independiente y que el cambio de uno afecte lo menos posible al resto.
Obviamente cuando dos clases se relacionan entre sí para trabajar juntas (y esto tiene que ocurrir sí o sí), va a existir un acoplamiento entre ellas.
Pero existen distintos niveles de acoplamiento, y gracias a algunos de los Principios SOLID, podemos relajar esas dependencias y hacerlas mucho más flexibles a cambios.
¹https://twitter.com/unclebobmartin ²http://devexperto.com/agile-software-development ³http://blog.cleancoder.com/
Principios SOLID: Qué son, cuáles, y qué beneficios aporta usarlo 4
¡Claro! Ahí está la gracia. Aún no hemos hablado de los Principios a fondo, pero por poner un ejemplo:
Imagina que tienes una clase A que tiene un acoplamiento muy fuerte con una clase B, de tal forma que cada vez que cambia B inevitablemente tiene que cambiar A.
Esto es muy posible que esté incumpliendo el Principio de Responsabilidad Única. La forma de cumplirlo sería haciendo que cuando B cambie, A no lo haga. Y para esto, la solución puede ser aplicar el Principio de Inversión de Dependencias.
¡No pasa nada! Esto es muy normal y te pasará casi siempre.
Un mismo problema se puede resolver desde dos perspectivas distintas en función de en qué Principio nos enfoquemos cuando lo resolvamos. Pero el resultado será el mismo.
Esto es lo más difícil de aceptar: muchas veces es imposible cumplir todos los Principios a la vez.
Porque al aplicar un Principio, se puede estar dando la espalda a otro.
Pero no te obsesiones con esto: al final lo importante es entender la potencia de cada Principio.
Y ese conocimiento unido a la experiencia te irá diciendo poco a poco cuáles son las mejores decisiones a tomar.
Ahora que ya tenemos claros los conceptos, puedes empezar a entenderlos uno a uno y aprender cómo aplicarlos.
El Principio de responsabilidad única es el primero de los cinco que componen SOLID. El principio de Responsabilidad Única nos viene a decir que un objeto debe realizar una única cosa. Es muy habitual, si no prestamos atención a esto, que acabemos teniendo clases que tienen varias responsabilidades lógicas a la vez. Si lo has visto por ahí, seguramente hayas una frase similar a esta:
El Principio de Responsabilidad Única nos dice que un módulo tiene una única razón para cambiar
En estos artículos verás que uso de forma indiferente las palabras módulo, entidad o clase. En realidad, cuando hablamos de lenguajes orientados a objetos, esto siempre se refiere a una clase. A mí esta definición no me gusta mucho, porque lo de “una única razón para cambiar” me suena muy etéreo, y es difícil bajarlo a tierra. Prefiero decir que el Principio de Responsabilidad Única se cumple cuando nuestra clase solo hace una cosa. Tampoco es fácil definir qué es “una cosa”, pero ya tenemos herramientas más sencillas para detectarlo. Por ejemplo, si cuando tienes que explicar el funcionamiento de una clase, dices que “esta clase hace esta cosa Y esta otra”, entonces sospecha.
Pero te lo voy a poner más fácil, te voy a dar unos truquillos para mejorar la detección:
La respuesta a esta pregunta es bastante subjetiva. Sin necesidad de obsesionarnos con ello, podemos detectar situaciones en las que una clase podría dividirse en varias:
Principio de Responsabilidad Única 7
Un ejemplo típico es el de un objeto que necesita ser renderizado de alguna forma, por ejemplo imprimiéndose por pantalla. Podríamos tener una clase como esta:
1 class Vehicle( 2 val wheelCount: Int, 3 val maxSpeed: Int 4 ) { 5 fun print() { 6 println("wheelCount=$wheelCount, maxSpeed=$maxSpeed") 7 } 8 }
Aunque a primera vista puede parecer una clase de lo más razonable, en seguida podemos detectar que estamos mezclando dos conceptos muy diferentes: la lógica de negocio y la lógica de presentación. Este código nos puede dar problemas en muchas situaciones distintas:
Hay casos como este que se ven muy claros, pero muchas veces los detalles serán más sutiles y probablemente no los detectarás a la primera. No tengas miedo de refactorizar lo que haga falta para que se ajuste a lo que necesites. Un solución muy simple sería crear una clase que se encargue de imprimir:
Principio de Responsabilidad Única 8
1 class VehiclePrinter { 2 fun print(vehicle: Vehicle) { 3 println( 4 "wheelCount=${vehicle.wheelCount}, " + 5 "maxSpeed=${vehicle.maxSpeed}" 6 ) 7 } 8 }
Si necesitases distintas variaciones para presentar la misma clase de forma diferente (por ejemplo, texto plano y HTML), siempre puedes crear una interfaz y crear implementaciones específicas. Pero ese es un tema diferente. Otro ejemplo que nos podemos encontrar a menudo es el de objetos a los que les añadimos el método save(). Una vez más, la capa de lógica y la de persistencia deberían permanecer separadas. Seguramente hablaremos mucho de esto en futuros artículos.
El Principio de Responsabilidad Única es una herramienta indispensable para proteger nuestro código frente a cambios, ya que implica que sólo debería haber un motivo por el que modificar una clase. En la práctica, muchas veces nos encontraremos con que estos límites tendrán más que ver con lo que realmente necesitemos que con complicadas técnicas de disección. Tu código te irá dando pistas según el software evolucione. ¿Crees que lo podrás aplicar a partir de ahora en tu día a día?
Principio Open/Closed 10
Siguiendo con nuestro ejemplo de vehículos, podríamos tener la necesidad de dibujarlos en pantalla. Imaginemos que tenemos una clase con un método que se encarga de dibujar un vehículo por pantalla. Por supuesto, cada vehículo tiene su propia forma de ser pintado. Nuestro vehículo tiene la siguiente forma:
1 class Vehicle(val type: VehicleType)
Básicamente es una clase que especifica su tipo mediante un enumerado. Podemos tener por ejemplo un enum con un par de tipos:
1 enum class VehicleType { 2 CAR, MOTORBIKE 3 }
Y éste es el método de la clase que se encarga de pintarlos:
1 fun draw(vehicle: Vehicle) { 2 when(vehicle.type){ 3 VehicleType.CAR -> drawCar(vehicle) 4 VehicleType.MOTORBIKE -> drawMotorbike(vehicle) 5 } 6 }
Mientras no necesitemos dibujar más tipos de vehículos ni veamos que este when se repite en varias partes de nuestro código, en mi opinión no debes sentir la necesidad de modificarlo. Incluso el hecho de que cambie la forma de dibujar un coche o una moto estaría encapsulado en sus propios métodos y no afectaría al resto del código. Pero puede llegar un punto en el que necesitemos dibujar un nuevo tipo de vehículo, y luego otro… Esto implica crear un nuevo enumerado, un nuevo case y un nuevo método para implementar el dibujado. En este caso sería buena idea aplicar el
Si lo solucionamos mediante herencia o polimorfismo⁷, el paso evidente es sustituir ese enumerado por clases reales, y que cada clase sepa cómo pintarse: ⁷https://devexperto.com/herencia-vs-composicion/
Principio Open/Closed 11
1 interface Vehicle { 2 fun draw() 3 } 4 5 class Car : Vehicle { 6 override fun draw() { 7 // Draw the car 8 } 9 } 10 11 class Motorbike : Vehicle { 12 override fun draw() { 13 // Draw the motorbike 14 } 15 }
Ahora nuestro método anterior se reduce a:
1 fun draw(vehicle: Vehicle) { 2 vehicle.draw() 3 }
Añadir nuevos vehículos ahora es tan sencillo como crear la clase correspondiente que extienda de Vehicle:
1 class Truck : Vehicle { 2 override fun draw() { 3 // Draw the truck 4 } 5 }
Como puedes ver, este ejemplo choca directamente con el que vimos en el Principio de Responsabilidad Única. Esta clase está guardando la información del objeto y la forma de pintarlo. ¿Implica eso que es incorrecto?
El principio de sustitución de Liskov nos dice que si en alguna parte de nuestro código estamos usando una clase, y esta clase es extendida, tenemos que poder utilizar cualquiera de las clases hijas y que el programa siga siendo válido.
Esto nos obliga a asegurarnos de que cuando extendemos una clase no estamos alterando el comportamiento de la padre.
Este principio viene a desmentir la idea preconcebida de que las clases son una forma directa de modelar la realidad, y que hay que tener cuidado con esa modelización.
La primera en hablar de él fue Bárbara Liskov⁸ (de ahí el nombre), una reconocida ingeniera de software americana.
Seguro que te has encontrado con esta situación muchas veces: creas una clase que extiende de otra, pero de repente uno de los métodos te sobra, y no sabes que hacer con él.
Las opciones más rápidas son bien dejarlo vacío, bien lanzar una excepción cuando se use, asegurándote de que nadie llama incorrectamente a un método que no se puede utilizar.
Si un método sobrescrito no hace nada o lanza una excepción, es muy probable que estés violando el principio de sustitución de Liskov.
Si tu código estaba usando un método que para algunas concreciones ahora lanza una excepción, ¿cómo puedes estar seguro de que todo sigue funcionando?
⁸https://en.wikipedia.org/wiki/Barbara_Liskov
Otra herramienta que te avisará fácilmente son los tests. Si los tests de la clase padre no funcionan para la hija, también estarás violando este principio. Veremos un ejemplo con el primer caso.
En la vida real tenemos claro que un elefante es un animal. Imaginemos que tenemos la clase Animal que representa un animal, y les damos a los animales la propiedad de andar y saltar:
1 open class Animal { 2 open fun walk() { ... } 3 open fun jump() { ... } 4 }
Y tenemos una parte del código donde recibimos un animal, y necesitamos que el animal salte:
1 fun jumpHole(a: Animal){ 2 a.walk() 3 a.jump() 4 a.walk() 5 }
Ahora nos creamos un elefante. Pero claro, un elefante no puede saltar, así que decidimos lanzar una excepción para asegurarnos de detectarlos si esto ocurre:
1 class Elephant : Animal() { 2 override fun jump() = 3 throw Exception("Los elefantes no pueden saltar") 4 5 }
Ahora en todos los sitios donde estemos usando jumpHole(), si el animal es un elefante, tendremos una excepción. Mal asunto, ¿no?
Principio de sustitución de Liskov 16
1 fun jumpHole(a: LightweightAnimal){ 2 a.walk() 3 a.jump() 4 a.walk() 5 }
Elegir las abstracciones correctas muchas veces no es fácil, pero tenemos que intentar limitar al máximo cuál es su alcance para no pedir más de lo que se necesita ni menos. Esta es la solución que obtendríamos mediante herencia aplicando el Principio de Liskov, pero también se podría haber solucionado mediante composición. La herencia nos puede generar una jerarquía de clases muy compleja si hay muchos tipos de animales, así que en función del problema hay que plantearse cuál merece la pena usar. Esta segunda opción es la que veremos con el Principio de segregación de interfaces.
El principio de Liskov nos ayuda a utilizar la herencia de forma correcta, y a tener mucho más cuidado a la hora de extender clases. En la práctica nos ahorrará muchos errores derivados de nuestro afán por modelar lo que vemos en la vida real en clases siguiendo la misma lógica. No siempre hay una modelización exacta, por lo que este principio nos ayudará a descubrir la mejor forma de hacerlo.
El principio de segregación de interfaces viene a decir que ninguna clase debería depender de métodos que no usa. Por tanto, cuando creemos interfaces que definan comportamientos, es importante estar seguros de que todas las clases que imple- menten esas interfaces vayan a necesitar y ser capaces de agregar comportamientos a todos los métodos. En caso contrario, es mejor tener varias interfaces más pequeñas.
Las interfaces nos ayudan a desacoplar módulos entre sí. Esto es así porque si tenemos una interfaz que explica el comportamiento que el módulo espera para comunicarse con otros módulos, nosotros siempre podremos crear una clase que lo implemente de modo que cumpla las condiciones.
El módulo que describe la interfaz no tiene que saber nada sobre nuestro código y, sin embargo, nosotros podemos trabajar con él sin problemas.
La problemática surge cuando esas interfaces intentan definir más cosas de las debidas, lo que se denominan fat interfaces.
Probablemente ocurrirá que las clases hijas acabarán por no usar muchos de esos métodos, y habrá que darles una implementación.
Muy habitual es lanzar una excepción, o simplemente no hacer nada.
Pero, al igual que vimos en algún ejemplo en el principio de sustitución de Liskov, esto es peligroso. Si lanzamos una excepción, es más que probable que el módulo que define esa interfaz use el método en algún momento, y esto hará fallar nuestro programa.
El resto de implementaciones “por defecto” que podamos dar, pueden generar efectos secundarios que no esperemos, y a los que sólo podemos responder conociendo el código fuente del módulo en cuestión, cosa que no nos interesa.