Principios SOLID en C#
Aplicación de los principios SOLID en C# con ejemplos: código más flexible, mantenible y comprobable.
En el desarrollo de software, los principios SOLID, definidos por Robert C. Martin, son cinco reglas fundamentales que buscan crear sistemas más flexibles, legibles y mantenibles en la programación orientada a objetos (POO). En C#, estos principios se aplican ampliamente como guías en el diseño de clases.
1. Single Responsibility Principle (SRP) – Principio de responsabilidad única
Una clase debe tener una sola responsabilidad. Es decir, debe existir una única razón para que cambie. Si una clase realiza múltiples tareas (ej. guardar datos + generar reportes), su mantenimiento se vuelve difícil y aumenta la probabilidad de errores.
// Incorrecto: Guarda pedidos e imprime facturas
public class OrderManager
{
public void SaveOrder(Order order) { /* guardar */ }
public void PrintInvoice(Order order) { /* imprimir */ }
}
// Correcto: Cada clase tiene una sola responsabilidad
public class OrderRepository
{
public void Save(Order order) { /* guardar */ }
}
public class InvoicePrinter
{
public void Print(Order order) { /* imprimir */ }
}
2. Open/Closed Principle (OCP) – Principio Abierto/Cerrado
Las clases deben estar abiertas a la extensión, pero cerradas a la modificación.
Esto significa que se deben poder agregar nuevas funcionalidades sin cambiar el código existente.
Generalmente se logra mediante abstract class o interface.
// Incorrecto: Agregar un nuevo método de pago obliga a modificar la clase
public class PaymentService
{
public void Pay(string type)
{
if (type == "CreditCard") { /* ... */ }
else if (type == "PayPal") { /* ... */ }
}
}
// Correcto: Basta con crear una nueva clase
public interface IPayment
{
void Pay();
}
public class CreditCardPayment : IPayment
{
public void Pay() { /* ... */ }
}
public class PayPalPayment : IPayment
{
public void Pay() { /* ... */ }
}
3. Liskov Substitution Principle (LSP) – Principio de sustitución de Liskov
Las clases derivadas deben poder usarse en lugar de sus clases base. Es decir, una subclase debe funcionar correctamente en cualquier lugar donde se espere su superclase. Una subclase no debe violar el contrato de su clase base.
public abstract class Bird
{
public abstract void Fly();
}
public class Sparrow : Bird
{
public override void Fly() { Console.WriteLine("El gorrión vuela"); }
}
// Incorrecto: Los pingüinos no vuelan, pero Fly() es obligatorio
public class Penguin : Bird
{
public override void Fly() { throw new NotImplementedException(); }
}
Aquí la clase Penguin viola el LSP.
Porque la clase abstracta Bird impone la suposición de que “todas las aves vuelan”.
En su lugar, se deben definir interfaces separadas como IFlyingBird e INonFlyingBird.
4. Interface Segregation Principle (ISP) – Principio de segregación de interfaces
Una clase no debe verse obligada a implementar interfaces grandes con métodos que no necesita. En su lugar, se deben usar interfaces más pequeñas y específicas.
// Incorrecto: Todos los animales deben caminar, volar y nadar
public interface IAnimal
{
void Walk();
void Fly();
void Swim();
}
// Correcto: Interfaces más pequeñas
public interface IWalkable { void Walk(); }
public interface IFlyable { void Fly(); }
public interface ISwimmable { void Swim(); }
public class Dog : IWalkable, ISwimmable
{
public void Walk() { /* ... */ }
public void Swim() { /* ... */ }
}
5. Dependency Inversion Principle (DIP) – Principio de inversión de dependencias
Las clases de alto nivel no deben depender directamente de las clases de bajo nivel. Ambas deben depender de abstracciones (interfaces o clases abstractas). Este principio es la base de la inyección de dependencias.
// Incorrecto: Service depende directamente de ConsoleLogger
public class Service
{
private readonly ConsoleLogger _logger = new ConsoleLogger();
}
// Correcto: Depende de una abstracción
public interface ILogger
{
void Log(string msg);
}
public class ConsoleLogger : ILogger
{
public void Log(string msg) => Console.WriteLine(msg);
}
public class Service
{
private readonly ILogger _logger;
public Service(ILogger logger) { _logger = logger; }
}
Ventajas
- Mejora la legibilidad y mantenibilidad del código.
- Proporciona una arquitectura más resistente a los cambios.
- Facilita las pruebas unitarias.
- Reduce la duplicación de código y aumenta la reutilización.
TL;DR
- SRP: Cada clase debe tener una sola responsabilidad.
- OCP: Abierto a la extensión, cerrado a la modificación.
- LSP: Las subclases deben poder sustituir a las superclases.
- ISP: Interfaces pequeñas y enfocadas en lugar de grandes.
- DIP: Depender de abstracciones, no de clases concretas.
Artículos relacionados
Clases, Objetos, Propiedades y Métodos en C#
Aprende cómo las clases, objetos, propiedades y métodos en C# forman la base de la programación orientada a objetos.
Encapsulación, Herencia y Polimorfismo en C#
Aprende encapsulación, herencia y polimorfismo en C# con ejemplos claros para dominar los principios básicos de la POO.
Fundamentos de Inyección de Dependencias en C#
Aprende los fundamentos de Inyección de Dependencias en C#, gestionando dependencias y logrando bajo acoplamiento.
Interfaces y Clases Abstractas en C#
Aprende interfaces y clases abstractas en C#, sus diferencias y cuándo usar cada una para diseñar código limpio y extensible.
Manejo de excepciones en C# (try, catch, finally)
Aprende a manejar excepciones en C# usando bloques try, catch y finally para gestionar errores de forma segura con ejemplos.
Métodos de Extensión en C#
Aprende métodos de extensión en C# para añadir nuevas funcionalidades a tipos existentes sin modificar su código.