Principios SOLID aplicados en JavaScript
Aprende los cinco principios SOLID para crear código orientado a objetos mantenible, escalable y robusto en JavaScript.
TL;DR - Resumen rápido
- S: Single Responsibility Principle - cada clase debe tener una sola razón para cambiar
- O: Open/Closed Principle - abierto para extensión, cerrado para modificación
- L: Liskov Substitution Principle - las subclases deben ser sustituibles por sus superclases
- I: Interface Segregation Principle - las interfaces deben ser específicas, no generales
- D: Dependency Inversion Principle - depende de abstracciones, no de implementaciones concretas
Introducción a los Principios SOLID
Los principios SOLID son cinco principios de diseño orientado a objetos que ayudan a crear software más mantenible, escalable y robusto. Introducidos por Robert C. Martin (Uncle Bob) en los años 2000, estos principios se han convertido en estándares de la industria y se aplican tanto a lenguajes con tipado fuerte como a JavaScript.
Aunque SOLID fue originalmente diseñado para lenguajes orientados a objetos clásicos como Java y C++, sus principios son aplicables a JavaScript moderno. En JavaScript, donde las clases y la herencia son opcionales y la composición se prefiere sobre la herencia, SOLID se adapta de manera natural, especialmente cuando se trabaja con clases ES6 y patrones de diseño.
- Mejora la mantenibilidad del código a largo plazo
- Facilita la extensión sin modificar código existente
- Reduce el acoplamiento entre componentes
- Hace el código más testeable
- Promueve arquitecturas más limpias y organizadas
S - Single Responsibility Principle (SRP)
El Principio de Responsabilidad Única establece que una clase debe tener una, y solo una, razón para cambiar. En otras palabras, una clase debe hacer una sola cosa y hacerla bien. Si una clase tiene múltiples responsabilidades, cualquier cambio en cualquiera de esas responsabilidades requiere modificar la clase, aumentando el riesgo de introducir bugs.
Violación del SRP
Una violación común del SRP es cuando una clase maneja múltiples responsabilidades no relacionadas, como validación, persistencia y presentación. Esto crea clases que hacen demasiadas cosas y son difíciles de mantener y probar.
La clase Usuario hace demasiadas cosas: valida datos, guarda en base de datos y envía emails. Si cambiamos la lógica de validación, modificamos la clase. Si cambiamos cómo guardamos en base de datos, modificamos la clase. Cada cambio afecta funcionalidades no relacionadas, aumentando el riesgo de bugs.
Aplicación Correcta del SRP
Para aplicar SRP correctamente, dividimos la clase en múltiples clases, cada una con una responsabilidad única. La clase Usuario solo representa datos de usuario, UsuarioValidator valida usuarios, UsuarioRepository los guarda, y EmailService envía emails.
Ahora cada clase tiene una sola responsabilidad. Usuario representa datos, UsuarioValidator valida, UsuarioRepository persiste, y EmailService envía emails. Cambios en validación no afectan persistencia, y cambios en persistencia no afectan validación. El código es más modular, testeable y mantenible.
Beneficios del SRP
Menos acoplamiento: Las clases son independientes
Más fácil de probar: Cada clase puede probarse aisladamente
Cambios localizados: Los cambios afectan solo una responsabilidad
Mayor reutilización: Las clases especializadas son más reutilizables
O - Open/Closed Principle (OCP)
El Principio Abierto/Cerrado establece que las entidades de software (clases, módulos, funciones) deben estar abiertas para extensión pero cerradas para modificación. En otras palabras, debes poder agregar nueva funcionalidad extendiendo el código existente, no modificándolo. Esto reduce el riesgo de introducir bugs en código que ya funciona.
Violación del OCP
Una violación común del OCP es usar condicionales (if/else o switch) para manejar diferentes tipos o comportamientos. Cada vez que necesitas agregar un nuevo tipo, debes modificar el código existente, violando el principio de estar cerrado para modificación.
La función calcularDescuento usa un switch para manejar diferentes tipos de cliente. Si agregamos un nuevo tipo de cliente, debemos modificar la función, lo que puede introducir bugs en los casos existentes. Cada nuevo tipo requiere modificar el código, violando OCP.
Aplicación Correcta del OCP
Para aplicar OCP correctamente, usamos polimorfismo y abstracción en lugar de condicionales. Creamos una interfaz o clase base que define el comportamiento, y cada tipo implementa su propia versión. Agregar un nuevo tipo es simplemente agregar una nueva implementación, sin modificar código existente.
Ahora cada tipo de cliente tiene su propia clase que implementa el método calcularDescuento. Agregar un nuevo tipo de cliente es simplemente crear una nueva clase, sin modificar el código existente. El código está abierto para extensión (nuevos tipos) pero cerrado para modificación (no cambiamos el código existente). En JavaScript, OCP se aplica naturalmente con funciones de orden superior y composición, pasando funciones como parámetros para extender comportamiento.
L - Liskov Substitution Principle (LSP)
El Principio de Sustitución de Liskov establece que las subclases deben ser sustituibles por sus superclases sin alterar la corrección del programa. En otras palabras, si tienes una clase Base y una clase Derivada que hereda de Base, debes poder usar Derivada en cualquier lugar donde esperas Base, sin que el programa se comporte de manera inesperada.
Violación del LSP
Una violación común del LSP es cuando una subclase cambia el comportamiento esperado de la superclase o lanza excepciones que la superclase no lanza. Esto rompe el contrato implícito de la superclase y hace que el código que usa la superclase falle inesperadamente cuando se usa la subclase.
La clase Pinguino hereda de Ave, pero no puede volar. El código que espera un Ave que puede volar fallará cuando reciba un Pinguino. Esto viola LSP porque Pinguino no es un sustituto válido de Ave en todos los contextos donde se usa Ave.
Advertencia sobre LSP
LSP es particularmente importante en JavaScript porque no hay tipado fuerte para prevenir violaciones. Debes documentar claramente el contrato de tus clases y asegurar que las subclases lo respeten completamente.
Aplicación Correcta del LSP
Para aplicar LSP correctamente, asegúrate de que las subclases respeten completamente el contrato de la superclase. Si una subclase no puede implementar todos los métodos de la superclase, considera usar composición en lugar de herencia, o dividir la superclase en interfaces más específicas.
Ahora tenemos AveVoladora y AveNoVoladora como interfaces separadas. Pinguino implementa AveNoVoladora, y Canario implementa AveVoladora. El código que usa AveNoVoladora funciona correctamente con Pinguino, y el código que usa AveVoladora funciona con Canario. No hay violación de LSP.
Composición sobre herencia
Cuando LSP es difícil de cumplir, considera usar composición en lugar de herencia. La composición permite construir objetos combinando comportamientos sin las restricciones de la herencia.
I - Interface Segregation Principle (ISP)
El Principio de Segregación de Interfaces establece que las interfaces deben ser específicas, no generales. Los clientes no deben depender de interfaces que no usan. En otras palabras, es mejor tener muchas interfaces pequeñas y específicas que una interfaz grande y general que obliga a los clientes a implementar métodos que no necesitan.
Violación del ISP
Una violación común del ISP es crear interfaces o clases "god" que tienen demasiados métodos. Los clientes que implementan estas interfaces deben proporcionar implementaciones vacías o lanzar excepciones para métodos que no usan, lo que es confuso y propenso a errores.
La interfaz Trabajador tiene métodos para múltiples tipos de trabajadores. Un Robot implementa Trabajador pero no puede comer o dormir, por lo que sus implementaciones de estos métodos son vacías o lanzan excepciones. Esto viola ISP porque Robot está forzado a implementar métodos que no usa.
Aplicación Correcta del ISP
Para aplicar ISP correctamente, divide las interfaces grandes en interfaces más pequeñas y específicas. Cada cliente implementa solo las interfaces que necesita. Esto crea contratos más claros y evita implementaciones vacías o excepciones innecesarias.
Ahora tenemos interfaces separadas: Trabajador, TrabajadorHumano y TrabajadorMaquina. Humano implementa Trabajador y TrabajadorHumano, mientras que Robot implementa Trabajador y TrabajadorMaquina. Cada clase implementa solo los métodos que realmente necesita, sin implementaciones vacías.
ISP en JavaScript
En JavaScript, donde no hay interfaces explícitas, ISP se aplica separando clases y objetos en contratos más específicos. En lugar de crear clases grandes, crea clases pequeñas y especializadas que pueden combinarse mediante composición.
D - Dependency Inversion Principle (DIP)
El Principio de Inversión de Dependencias establece que los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones. Además, las abstracciones no deben depender de detalles. Los detalles deben depender de las abstracciones. En JavaScript, esto significa depender de interfaces o contratos, no de implementaciones concretas.
Violación del DIP
Una violación común del DIP es cuando las clases de alto nivel dependen directamente de clases de bajo nivel concretas. Esto crea acoplamiento fuerte y hace difícil cambiar la implementación de bajo nivel sin afectar el código de alto nivel.
La clase Pedido depende directamente de MySQLDatabase. Si queremos cambiar a PostgreSQL, debemos modificar Pedido. Esto crea acoplamiento fuerte y hace difícil cambiar la implementación de base de datos. Pedido depende de un detalle concreto (MySQLDatabase) en lugar de una abstracción.
Aplicación Correcta del DIP
Para aplicar DIP correctamente, crea una abstracción (interfaz o contrato) que define las operaciones necesarias. Las clases de alto nivel dependen de la abstracción, y las clases de bajo nivel implementan la abstracción. Esto permite cambiar la implementación sin afectar el código de alto nivel.
Ahora Pedido depende de la abstracción Database, no de MySQLDatabase. MySQLDatabase y PostgreSQLDatabase implementan Database. Para cambiar de base de datos, simplemente cambiamos qué implementación de Database inyectamos a Pedido, sin modificar el código de Pedido.
Inyección de Dependencias
La inyección de dependencias es la técnica principal para aplicar DIP. En lugar de que una clase cree sus propias dependencias, recíbelas como parámetros del constructor o mediante setters. Esto facilita el testing y permite cambiar implementaciones fácilmente.
Aplicación de SOLID en JavaScript
Los principios SOLID se originaron en lenguajes orientados a objetos clásicos, pero se aplican perfectamente a JavaScript moderno. En JavaScript, donde las clases son opcionales y la composición se prefiere sobre la herencia, SOLID se adapta de manera natural. La clave es entender el espíritu de los principios y aplicarlos de manera idiomática para JavaScript.
- <strong>Usa clases cuando:</strong> trabajas con POO, necesitas herencia clara, o el dominio se modela mejor con objetos
- <strong>Usa funciones cuando:</strong> trabajas con transformación de datos, necesitas composición flexible, o prefieres programación funcional
- <strong>Combina ambos enfoques:</strong> JavaScript permite usar clases y funciones juntos, aprovecha lo mejor de ambos paradigmas
- <strong>Prioriza simplicidad:</strong> no apliques SOLID si agrega complejidad innecesaria, úsalo cuando mejore mantenibilidad
SOLID con Funciones y Composición
En JavaScript, muchos principios SOLID se aplican mejor con funciones y composición que con clases y herencia. Las funciones de orden superior, el curry y la composición permiten crear código modular y extensible sin la complejidad de las jerarquías de clases.
El ejemplo muestra cómo aplicar SOLID con funciones. Cada función tiene una responsabilidad única (SRP), las funciones son extensibles mediante composición (OCP), las funciones pueden sustituirse entre sí (LSP), las funciones son pequeñas y específicas (ISP), y las funciones dependen de otras funciones, no de implementaciones concretas (DIP). SOLID no prescribe clases ni herencia; prescribe principios de diseño que se aplican también a programación funcional.
Resumen: Principios SOLID
Conceptos principales:
- •S: Single Responsibility - cada clase debe tener una sola razón para cambiar
- •O: Open/Closed - abierto para extensión, cerrado para modificación
- •L: Liskov Substitution - las subclases deben ser sustituibles por superclases
- •I: Interface Segregation - las interfaces deben ser específicas, no generales
- •D: Dependency Inversion - depende de abstracciones, no de implementaciones concretas
Mejores prácticas:
- •Aplica SOLID con criterio: no sacrifiques simplicidad por principios
- •Prefiere composición sobre herencia en JavaScript
- •Usa inyección de dependencias para aplicar DIP
- •Divide interfaces grandes en interfaces más pequeñas
- •Escribe tests que verifiquen el cumplimiento de SOLID