SOLID: La guía completa de los 5 principios en la POO
SOLID es un acrónimo de los primeros cinco principios de diseño orientado a objetos (OOD) de Robert C. Martin (también conocido como Uncle Bob).
Estos principios establecen prácticas para desarrollar software con consideraciones de mantenimiento y extensibilidad a medida que el proyecto crece. Adoptar estas prácticas también puede ayudar a evitar “code smells”, refactorizar código y desarrollar software Ágil o Adaptativo.
SOLID significa:
- S – Single-responsibility Principle
- O – Open-closed Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
En este artículo, se presentará cada principio individualmente para entender cómo SOLID puede ayudarte a ser un mejor desarrollador.
Single-responsibility Principle (Principio de Responsabilidad Única)
El Principio de Responsabilidad Única establece que una clase debe tener una sola razón para cambiar, es decir, una única responsabilidad.
Por ejemplo, imagina una aplicación de gestión de facturas donde necesitas calcular el total de una factura y luego generar diferentes formatos de reporte. Para cumplir el SRP, separa estas responsabilidades en dos clases:
class CalculadoraFactura
{
private $facturas;
public function __construct(array $facturas = [])
{
$this->facturas = $facturas;
}
public function calcularTotal(): float
{
$total = 0;
foreach ($this->facturas as $factura) {
$total += $factura->monto;
}
return $total;
}
}
class GeneradorReporte
{
private $calculadora;
public function __construct(CalculadoraFactura $calculadora)
{
$this->calculadora = $calculadora;
}
public function generarJSON(): string
{
return json_encode(['total' => $this->calculadora->calcularTotal()]);
}
public function generarHTML(): string
{
return 'Total de factura: ' . $this->calculadora->calcularTotal() . '
';
}
}
// Uso:
$facturas = [
(object)['monto' => 100.50],
(object)['monto' => 200.75],
];
$calculadora = new CalculadoraFactura($facturas);
$reporte = new GeneradorReporte($calculadora);
echo $reporte->generarHTML();
echo $reporte->generarJSON();
En este ejemplo, CalculadoraFactura
se encarga únicamente de calcular el total de las facturas, iterando sobre el arreglo y sumando sus montos. La clase GeneradorReporte
recibe una instancia de CalculadoraFactura
y es responsable solo de formatear ese resultado en JSON o HTML. De este modo, cada clase tiene una única responsabilidad y cambia por motivos distintos: la calculadora cambia si cambia la lógica de suma, y el generador de reportes cambia si cambia el formato de salida.
Open-closed Principle (Principio de Abierto/Cerrado)
El Principio de Abierto/Cerrado dicta que una entidad debe estar abierta para extensión, pero cerrada para modificación.
Supongamos que tenemos un sistema de descuentos. Mediante el patrón Estrategia, podemos agregar nuevos tipos de descuento sin modificar la clase principal:
interface EstrategiaDescuento
{
public function calcular(float $monto): float;
}
class DescuentoVerano implements EstrategiaDescuento
{
public function calcular(float $monto): float
{
return $monto * 0.90; // 10% de descuento
}
}
class DescuentoNavidad implements EstrategiaDescuento
{
public function calcular(float $monto): float
{
return $monto * 0.80; // 20% de descuento
}
}
class GestorDescuentos
{
private $estrategia;
public function __construct(EstrategiaDescuento $estrategia)
{
$this->estrategia = $estrategia;
}
public function aplicar(float $monto): float
{
return $this->estrategia->calcular($monto);
}
}
// Uso:
$gestor = new GestorDescuentos(new DescuentoVerano());
echo $gestor->aplicar(150.00);
$gestor = new GestorDescuentos(new DescuentoNavidad());
echo $gestor->aplicar(150.00);
La interfaz EstrategiaDescuento
define el contrato para las distintas estrategias de descuento. Cada clase (como DescuentoVerano
o DescuentoNavidad
) extiende este comportamiento sin modificar GestorDescuentos
. Cuando quieras un nuevo descuento, simplemente creas otra implementación de EstrategiaDescuento
, manteniendo la clase gestionadora inalterada.
Liskov Substitution Principle (Principio de Sustitución de Liskov)
El Principio de Sustitución de Liskov establece que los objetos de una subclase deben poder reemplazar a los objetos de la clase padre sin alterar el correcto funcionamiento del programa.
Por ejemplo, definimos una interfaz Vehiculo
con un método avanzar
que devuelve kilómetros recorridos:
interface Vehiculo
{
public function avanzar(): int;
}
class Coche implements Vehiculo
{
public function avanzar(): int
{
return 100;
}
}
class Bicicleta implements Vehiculo
{
public function avanzar(): int
{
return 20;
}
}
function recorridoTotal(array $vehiculos): int
{
$total = 0;
foreach ($vehiculos as $vehiculo) {
$total += $vehiculo->avanzar();
}
return $total;
}
// Uso:
$vehiculos = [new Coche(), new Bicicleta()];
echo recorridoTotal($vehiculos); // 120
En esta implementación, tanto Coche
como Bicicleta
cumplen el contrato de Vehiculo
. La función recorridoTotal
no necesita diferenciar entre ellos: puede sustituir cualquier instancia de Vehiculo
sin romperse. Esto demuestra que las subclases son intercambiables con la clase base.
Interface Segregation Principle (Principio de Segregación de Interfaces)
El Principio de Segregación de Interfaces establece que no se debe obligar a una clase a depender de métodos que no utiliza.
En lugar de una única interfaz con todos los métodos, dividimos la funcionalidad en interfaces más pequeñas:
interface Llamable
{
public function llamar(string $numero): void;
}
interface Fotografiable
{
public function tomarFoto(): void;
}
class TelefonoBasico implements Llamable
{
public function llamar(string $numero): void
{
// Lógica de llamada
}
}
class Smartphone implements Llamable, Fotografiable
{
public function llamar(string $numero): void
{
// Lógica de llamada
}
public function tomarFoto(): void
{
// Lógica de foto
}
}
// Uso:
$telefono = new TelefonoBasico();
$telefono->llamar('555-1234');
$movil = new Smartphone();
$movil->tomarFoto();
La interfaz Llamable
solo contiene el método llamar
, mientras que Fotografiable
contiene tomarFoto
. TelefonoBasico
no está forzado a implementar un método de foto que no necesita. Cada clase implementa únicamente las interfaces relevantes a su responsabilidad.
Dependency Inversion Principle (Principio de Inversión de Dependencias)
El Principio de Inversión de Dependencias dicta que las clases de alto nivel no deben depender de clases de bajo nivel, sino de abstracciones.
Veamos un ejemplo de envío de notificaciones por correo:
interface ProveedorCorreo
{
public function enviar(string $mensaje): bool;
}
class SMTPProveedor implements ProveedorCorreo
{
public function enviar(string $mensaje): bool
{
// Enviar correo vía SMTP
return true;
}
}
class EnviadorNotificaciones
{
private $proveedor;
public function __construct(ProveedorCorreo $proveedor)
{
$this->proveedor = $proveedor;
}
public function notificar(string $mensaje): bool
{
return $this->proveedor->enviar($mensaje);
}
}
// Uso:
$proveedor = new SMTPProveedor();
$enviador = new EnviadorNotificaciones($proveedor);
$enviador->notificar('Tu pedido ha sido enviado.');
Aquí EnviadorNotificaciones
depende de la abstracción ProveedorCorreo
en lugar de una implementación concreta. Si mañana quieres usar otro servicio (por ejemplo, una API REST), solo creas una nueva clase que implemente ProveedorCorreo
y la inyectas, sin tocar la lógica de notificaciones.
Conclusión
Hemos visto ejemplos que cumplen al 100% cada principio SOLID usando nombres y variables en español. Aplicar estos principios facilita la mantenibilidad, extensibilidad y testeo de tu código.
Para profundizar, explora prácticas Ágiles en Agile y el desarrollo de software adaptativo en Adaptive software development.