- Los principios SOLID definen cinco guías de diseño orientado a objetos que mejoran claridad, extensibilidad y testabilidad del código.
- Aplicar SRP, OCP, LSP, ISP y DIP en Python implica separar responsabilidades, depender de abstracciones y diseñar jerarquías coherentes.
- Un diseño SOLID se combina con DRY, KISS y YAGNI para lograr sistemas mantenibles sin sobre-diseño, ajustados al contexto real del proyecto.

En las siguientes líneas vamos a ver de forma muy detallada qué es SOLID, de dónde viene, para qué sirve en el día a día con Python y cómo aplicarlo con ejemplos reales en este lenguaje. Además, integraremos otros principios clave como DRY, KISS o YAGNI y comentaremos en qué contextos quizá no merece la pena ser ultra purista con SOLID.
Qué es SOLID y de dónde sale
Cuando hablamos de SOLID nos referimos a cinco principios clásicos de diseño orientado a objetos que se centran en cómo estructurar clases, módulos e interfaces para que el software sea fácil de extender, probar y mantener.
Estos principios se popularizaron gracias a Robert C. Martin (Uncle Bob), uno de los padres del desarrollo ágil y autor de libros como Clean Code o Clean Architecture. En los años 90 y principios de los 2000 publicó varios artículos sobre diseño orientado a objetos, entre ellos “The Principles of OOD” y “Design Principles and Design Patterns”.
Más adelante, el ingeniero Michael Feathers propuso el acrónimo SOLID para referirse al conjunto de cinco principios más importantes que se desprendían de esos trabajos. La idea era tener una regla mnemotécnica sencilla que cualquier desarrollador pudiera recordar y aplicar a su diseño de clases.
Las siglas corresponden a:
- S – Single Responsibility Principle (SRP, Principio de Responsabilidad Única).
- O – Open-Closed Principle (OCP, Principio de Abierto/Cerrado).
- L – Liskov Substitution Principle (LSP, Principio de Sustitución de Liskov).
- I – Interface Segregation Principle (ISP, Principio de Segregación de Interfaces).
- D – Dependency Inversion Principle (DIP, Principio de Inversión de Dependencias).
Aunque se formularon originalmente pensando en lenguajes fuertemente tipados como Java o C#, estos principios son perfectamente aplicables a Python, C++, PHP y, en general, a cualquier lenguaje con soporte para orientación a objetos.

Para qué sirven los principios SOLID en proyectos reales
Más allá de la teoría, SOLID se nota sobre todo cuando el proyecto crece y hay varios desarrolladores tocando el mismo código durante meses o años. Aplicar estos principios tiene varios beneficios muy claros:
Por un lado, mejoran la legibilidad y la claridad de la arquitectura: las clases tienen responsabilidades acotadas, las dependencias están controladas y resulta más fácil seguir el flujo de negocio sin perderse en detalles de infraestructura.
Además, favorecen la reutilización y la extensibilidad del código. Si las clases están bien diseñadas, añadir nuevas funcionalidades no implica desmontar medio sistema. Se tiende a extender mediante nuevas implementaciones en lugar de reescribir lo que ya funciona y está probado.
Otro punto clave es la testabilidad. Al reducir el acoplamiento y depender de abstracciones, es más sencillo inyectar dobles de prueba (mocks, stubs) y escribir tests unitarios rápidos que no dependen de bases de datos reales, llamadas de red ni otros sistemas externos.
Por último, una base de código que respeta SOLID sufre menos de code smells, code rot y “código espagueti”. Es decir, se degrada menos con el tiempo, acumula menos deuda técnica y resulta más seguro y estable en entornos críticos.
S – Single Responsibility Principle en Python
El Principio de Responsabilidad Única dice que una clase debe tener una única razón para cambiar. No significa que haga una sola cosa microscópica, sino que se responsabiliza de un área de funcionalidad bien definida.
Cuando una clase mezcla lógica de negocio, acceso a datos, formateo de informes y lógica de presentación, cada cambio en cualquiera de esas áreas obliga a tocar la misma pieza de código. Eso dispara el riesgo de errores cruzados y conflictos entre equipos.
Piensa, por ejemplo, en una clase User en Python que, además de representar al usuario, se encarga de conectarse a la base de datos, guardar sus datos y generar informes en texto. Esa clase tiene múltiples motivos para cambiar: si cambian los atributos del usuario, si cambiamos de base de datos o si se modifica el formato del informe.
La alternativa alineada con SRP es separar las responsabilidades: una clase o dataclass para el modelo de dominio del usuario, otra para la persistencia (repositorio o gateway) y otra para la generación de informes o vistas. Cada cambio cae en la pieza que le corresponde, reduciendo el acoplamiento.
Algo similar se ve con un ejemplo clásico de Python: una clase Duck que define el pato y, a la vez, maneja conversaciones entre patos mediante un método greet(). Esa parte de comunicación se puede extraer a un objeto Communicator, de forma que el pato solo sabe volar, nadar y hacer sonido, y el comunicador sabe cómo articular un diálogo entre dos aves.
Este enfoque encaja muy bien con otros principios como DRY (no repetir lógica en varias clases) y con la idea de mantener una cohesión alta dentro de cada módulo: todas sus funciones y atributos apuntan a una misma responsabilidad de negocio.
O – Open/Closed Principle aplicado a extensiones de comportamiento
El Principio de Abierto/Cerrado dice que las entidades de software deben estar abiertas a la extensión pero cerradas a la modificación. Traducido al día a día: deberíamos poder añadir nuevas variantes de comportamiento sin tener que editar el código que ya funciona.
Un anti-patrón típico en Python es tener una clase con un método que decide el comportamiento con un if/elif gigante en función del tipo: por ejemplo, un AreaCalculator que comprueba si el objeto es Rectangle, Circle, Triangle y así sucesivamente. Cada nueva figura geométrica obliga a meter otra rama en el método y tocar una clase ya probada.
Respetar OCP pasa por programar contra abstracciones. En este caso, definimos una clase abstracta Shape con un método area() y hacemos que cada figura (rectángulo, círculo, triángulo…) implemente dicho método. El calculador de áreas simplemente invoca shape.area() sin saber qué tipo concreto tiene delante.
Siguiendo con Python, otro ejemplo trabajado en varias fuentes es el de un objeto Communicator que se usa para conversaciones entre aves. Si queremos admitir distintas formas de conversar (simple, formal, con emojis, etc.), en lugar de modificar el método communicate() cada vez, definimos una abstracción de conversación (por ejemplo, AbstractConversation) y creamos implementaciones como SimpleConversation que generan las frases. El comunicador se limita a imprimir lo que le devuelva la conversación, sin modificar su propio código.
En el mundo empresarial, OCP es crucial para reducir el riesgo al introducir cambios: se evita reabrir clases base muy utilizadas y se favorece la extensión mediante nuevas subclases o estrategias, manteniendo el código estable lo más intacto posible.
L – Liskov Substitution Principle y herencia correcta
El Principio de Sustitución de Liskov establece que los objetos de una subclase deben poder sustituir a los de su superclase sin romper el sistema. Es decir, si un código espera una instancia de la clase base, debería funcionar igual si le damos una instancia de cualquier clase hija.
En la práctica, esto significa que la subclase no debe romper las expectativas o el contrato que marca la clase base: tipos de retorno, excepciones, precondiciones y postcondiciones deben seguir siendo coherentes.
Un ejemplo muy conocido (y tramposo) es el de la jerarquía Rectángulo / Cuadrado. Matemáticamente un cuadrado es un rectángulo, pero en código no siempre nos conviene que Square herede directamente de Rectangle. Si el rectángulo tiene setters independientes para alto y ancho, un método de prueba puede asumir que al cambiar solo el alto, el ancho se mantiene igual. Si la clase cuadrado fuerza que ambos cambien a la vez, de repente el contrato de la superclase deja de cumplirse.
Algo parecido ocurre en jerarquías como Bird en Python. Si la clase base incluye un método fly(), pero luego tenemos subclases como Ostrich o pingüinos que no pueden volar, forzar esa implementación y lanzar un NotImplementedError rompe LSP. Cualquier función que reciba un Bird y llame a fly() debería poder hacerlo sin excepciones sorpresa.
La solución consiste en rediseñar la jerarquía: se mantiene una clase base genérica Bird sin el método fly(), y se crean subclases intermedias como FlyingBird y SwimmingBird. Las aves que vuelan implementan la primera; las que nadan, la segunda; y las que hacen ambas heredan de las dos. De esta forma, ninguna subclase está obligada a ofrecer un comportamiento que no tiene sentido para ella.
Respetar LSP obliga a pensar muy bien las relaciones de herencia en Python, y en muchos casos lleva a preferir composición e interfaces simples frente a árboles de herencia profundos donde es fácil violar el contrato de la superclase.
I – Interface Segregation Principle y clases más específicas
El Principio de Segregación de Interfaces indica que no debemos obligar a un cliente a depender de métodos que no usa. En la práctica, esto se traduce en diseñar interfaces (o clases abstractas en Python) pequeñas y específicas en lugar de una única interfaz gigante para todo.
Imagina una interfaz Worker en Python que define los métodos work() y eat(). Esta interfaz tiene sentido para un humano que trabaja y come, pero no para un robot que solo trabaja. Si forzamos a la clase Robot a implementar eat(), acabará con un método vacío o con una excepción, indicando que “los robots no comen”, lo que es una señal clara de violación de ISP.
La solución consiste en dividir esa interfaz en Workable (algo que puede trabajar) y Eatable (algo que puede comer). Así, la clase Human implementa ambas, mientras que Robot implementa únicamente la parte de trabajo. Cada cliente depende solo de los métodos que realmente necesita.
Volviendo a las aves, ocurre algo parecido con una interfaz que combine métodos como fly() y swim(). No todas las aves hacen las dos cosas, así que tiene más sentido separar en FlyingBird y SwimmingBird. El diseño resultante es más flexible y evita tener que rellenar métodos irrelevantes con código ficticio.
En Python, aunque no tengamos interfaces formales como en Java, podemos conseguir el mismo efecto usando clases abstractas del módulo abc o protocolos de tipado. Segregar interfaces hace que cada clase quede más acotada y reduce la probabilidad de introducir dependencias innecesarias.
D – Dependency Inversion Principle y diseño basado en abstracciones
El Principio de Inversión de Dependencias señala dos ideas clave: los módulos de alto nivel no deben depender de módulos de bajo nivel, y ambos deben depender de abstracciones. Además, las abstracciones no deben depender de detalles; los detalles dependen de las abstracciones.
Un error muy común es que una clase de dominio o de lógica de negocio en Python cree directamente una instancia concreta de base de datos, servicio HTTP o cliente de mensajería: por ejemplo, un UserRepository que internamente hace self.database = MySQLDatabase(). Si el día de mañana queremos pasar a PostgreSQL o a un motor en memoria para testing, tendremos que editar esa clase.
Aplicar DIP implica introducir una abstracción de base de datos (por ejemplo, una clase abstracta Database con métodos connect() y query()) y crear implementaciones concretas como MySQLDatabase o PostgreSQLDatabase. El repositorio deja de instanciar él mismo la base de datos y pasa a recibirla desde fuera, normalmente por el constructor.
Este patrón se conoce como inyección de dependencias y es uno de los pilares para conseguir un código modular y testeable: en producción inyectamos una implementación real; en los tests inyectamos un doble de prueba que simula el comportamiento sin tocar sistemas externos.
En un ejemplo más rico, se puede definir una abstracción de canal de comunicación, como AbstractChannel, y una abstracción de comunicador, AbstractCommunicator, que trabaja con AbstractConversation. En lugar de que SMSCommunicator cree internamente un SMSChannel, la versión alineada con DIP hace que el comunicador reciba un canal abstracto en el constructor. Así, no depende de una clase concreta, sino de una interfaz, y puede funcionar con cualquier tipo de canal que cumpla el contrato.
En resumen, DIP anima a diseñar capas donde la lógica de negocio se apoya en interfaces genéricas, y son los frameworks, configuraciones o factorías quienes se encargan de enlazar esas interfaces con implementaciones específicas según el entorno.
Ejemplo práctico en Python: servicio de notificaciones SOLID
Un caso muy ilustrativo para ver cómo se combinan varios principios de diseño es el de un sistema de notificaciones que pueda enviar mensajes por distintos canales, como email o SMS, y que además sea fácil de ampliar con nuevos medios (push, webhooks, etc.).
El objetivo es que la lógica principal que decide “tengo que notificar X cosa” no tenga que reescribirse cada vez que añadimos un canal, y que podamos probarla sin disparar correos reales ni SMS durante los tests.
Para ello definimos una abstracción Notifier con un método send(to, subject, body) que devuelve un boolean indicando éxito o fracaso. A partir de ahí creamos implementaciones como EmailNotifier y SMSNotifier, cada una recibiendo en su constructor un cliente externo (por ejemplo, smtp_client o sms_client) encargado de hablar con el servicio real.
Encima de todo esto se construye un NotificationService que recibe una lista de notifiers (cualquier cosa que implemente el contrato) y ofrece un método para enviar una notificación por todos los canales. Este servicio no sabe nada de SMTP, de proveedores de SMS ni de APIs HTTP: solo itera y llama a send() sobre cada notificador.
En este diseño se ven claramente varios principios en acción:
- SRP: cada clase tiene una responsabilidad muy acotada: un
EmailNotifiersolo gestiona correos,SMSNotifiersolo SMS, el servicio orquesta, y los clientes externos solo saben de transporte. - DIP:
NotificationServicedepende de la abstracciónNotifier, no de clases concretas, y los notifiers dependen a su vez de clientes inyectados. - OCP: para añadir un
PushNotifieroWebhookNotifierno hace falta tocarNotificationService, basta con crear una nueva implementación y registrarla. - LSP e ISP: todas las implementaciones de
Notifierrespetan el mismo contratosend(), sin métodos obligatorios que no tengan sentido para un canal concreto.
Desde el punto de vista de tests, basta con crear uno o varios DummyNotifier que registren las llamadas a send() sin hacer nada más. Al inyectarlos en el servicio podemos comprobar que se han disparado todas las notificaciones sin necesidad de redes, colas ni sistemas externos.
Este ejemplo también se puede complementar con principios como DRY (evitar duplicar lógica común de formatos), KISS (no meter reintentos, colas o priorización hasta que realmente haga falta) y YAGNI (no anticipar funcionalidades que no tenemos en el roadmap inmediato).
Relación de SOLID con otros principios de diseño
Aunque SOLID se centra en la orientación a objetos, en la práctica se combina con otros principios generales de diseño de software como DRY, KISS y YAGNI, que también aparecen de forma recurrente en la literatura de código limpio.
DRY (Don’t Repeat Yourself) recuerda que la lógica de negocio importante debe tener una sola representación en el sistema. Encaja de maravilla con SRP y OCP: si repetimos comportamiento en distintas clases, extender o corregir un caso de uso obliga a modificar muchos sitios y aumenta el riesgo de inconsistencias.
KISS (Keep It Simple, Stupid) nos anima a evitar arquitecturas y abstracciones innecesariamente complejas. Aunque parezca contradictorio, aplicar SOLID no significa llenar el proyecto de interfaces por deporte, sino introducir las justas para mantener el diseño comprensible para el equipo.
YAGNI (You Aren’t Gonna Need It) es un antídoto contra el sobre-diseño. En proyectos con requisitos muy volátiles, intentar anticipar todos los posibles cambios futuros a base de capas y más capas puede ser contraproducente. Lo ideal es encontrar un equilibrio: aplicar SOLID en las partes que sabemos que van a crecer o cambiar, y mantener más sencillo lo que sabemos que es estable.
En conjunto, todos estos principios dan lugar a un estilo de desarrollo donde la prioridad es código claro, modular y orientado a la colaboración, pero sin caer en el fetichismo de la arquitectura por la arquitectura.
Ventajas, límites y casos en los que no compensa ser purista
Los principios SOLID se consideran casi una regla de oro en proyectos medianos y grandes, pero eso no significa que haya que aplicarlos al milímetro en todos los contextos. Conviene conocer también sus límites.
En aplicaciones muy pequeñas, scripts puntuales o proyectos de bajo presupuesto que no se van a mantener a largo plazo, introducir demasiadas capas de abstracción puede resultar más caro que beneficioso. El overhead mental y de código que supone aplicar DIP o ISP a todo puede no compensar.
También hay situaciones con requisitos de tiempo muy ajustados o desarrollo de un MVP donde interesa priorizar la velocidad de entrega. Tiene sentido posponer parte del refinamiento de SOLID para más adelante, cuando el producto se estabilice y sepamos qué partes van a sobrevivir.
En sistemas heredados con mucho código legacy, intentar “solidificar” todo de golpe suele ser poco realista. En estos escenarios es mejor ir aplicando los principios de manera incremental en las zonas del código que se tocan con frecuencia o que bloquean nuevas funcionalidades.
Por último, hay dominios muy sensibles al rendimiento extremo (por ejemplo, ciertas rutinas de alta frecuencia) donde las capas de abstracción pueden introducir una sobrecarga indeseable. En estos casos se justifica a veces sacrificar parte de la pureza de SOLID en bloques muy concretos, siempre que el resto del sistema se mantenga bien diseñado.
En conjunto, entender y aplicar los principios SOLID en Python permite pasar de un código que “solo funciona” a un código que aguanta bien el paso del tiempo, se adapta mejor a cambios de negocio, facilita la colaboración en equipo y reduce los sustos en producción; la clave está en usarlos con criterio, sabiendo cuándo merece la pena introducir una abstracción más y cuándo es mejor mantener las cosas simples y directas.
Redactor apasionado del mundo de los bytes y la tecnología en general. Me encanta compartir mis conocimientos a través de la escritura, y eso es lo que haré en este blog, mostrarte todo lo más interesante sobre gadgets, software, hardware, tendencias tecnológicas, y más. Mi objetivo es ayudarte a navegar por el mundo digital de forma sencilla y entretenida.
