SOLID

5 Ejemplos Principios SOLID en C#

SOLID es un acrónimo para cinco principios de diseño de software en orientado a objetos, los cuales son:

  1. Single responsibility principle (Principio de responsabilidad única): cada clase debe tener una única responsabilidad y esa responsabilidad debe estar completamente encapsulada por la clase.
  2. Open-closed principle (Principio abierto-cerrado): las clases deben estar diseñadas de tal manera que sean fácilmente extensibles sin modificar su código fuente.
  3. Liskov substitution principle (Principio de sustitución de Liskov): las clases derivadas deben ser sustituibles por sus clases base.
  4. Interface segregation principle (Principio de segregación de interfaces): las clases no deben ser forzadas a implementar interfaces que no utilizan.
  5. Dependency inversion principle (Principio de inversión de dependencias): las clases deberían depender de abstracciones y no de implementaciones concretas.

Principio de responsabilidad única

En este ejemplo, la clase Customer tiene dos responsabilidades: validar si el cliente es válido y guardar el cliente en la base de datos. En un diseño SOLID, estas dos responsabilidades deberían estar en clases diferentes, para cumplir con el principio de responsabilidad única. Por ejemplo, una clase CustomerValidator para la validación y otra clase CustomerRepository para la persistencia en la base de datos.

class Customer
{
    public string Name { get; set; }
    public string Email { get; set; }

    public bool Validate()
    {
        // Valida si el cliente es válido
    }

    public void Save()
    {
        // Guarda el cliente en la base de datos
    }
}

En el siguiente ejemplo, cada clase tiene una única responsabilidad: la clase CustomerValidator se encarga de validar si el cliente es válido, la clase CustomerRepository se encarga de guardar el cliente en la base de datos y la clase Customer contiene la información del cliente. De esta manera, se cumple con el principio de responsabilidad única ya que cada clase solo tiene una responsabilidad específica.

class CustomerValidator
{
    public bool Validate(Customer customer)
    {
        // Valida si el cliente es válido
    }
}

class CustomerRepository
{
    public void Save(Customer customer)
    {
        // Guarda el cliente en la base de datos
    }
}

class Customer
{
    public string Name { get; set; }
    public string Email { get; set; }
}

Principio abierto-cerrado

En este ejemplo, la clase Shape tiene un método para calcular el área, pero utiliza una estructura de control de flujo (if-else) para determinar el tipo de forma y calcular el área de manera diferente. Si se desea agregar una nueva forma, se deberá modificar el código de la clase Shape violando el principio de abierto-cerrado.

class Shape
{
    public double Area()
    {
        if (this is Rectangle)
        {
            return (this as Rectangle).Width * (this as Rectangle).Height;
        }
        else if (this is Circle)
        {
            return 3.14 * (this as Circle).Radius * (this as Circle).Radius;
        }
    }
}

class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
}

class Circle : Shape
{
    public double Radius { get; set; }
}

En el siguiente ejemplo, la clase Shape es una clase abstracta que tiene un método abstracto para calcular el área. Las clases Rectangle y Circle heredan de Shape y proporcionan implementaciones específicas para calcular el área. Con esta estructura, podemos agregar nuevas formas (como un triángulo o un círculo) sin modificar el código de la clase Shape, cumpliendo con el principio de abierto-cerrado.

abstract class Shape
{
    public abstract double Area();
}

class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public override double Area()
    {
        return Width * Height;
    }
}

class Circle : Shape
{
    public double Radius { get; set; }

    public override double Area()
    {
        return 3.14 * Radius * Radius;
    }
}

En resumen, el principio abierto-cerrado se refiere a que las clases deben diseñarse de tal manera que sean fácilmente extensibles sin modificar su código fuente. Una forma de lograrlo es a través de la abstracción y la herencia, permitiendo que las nuevas clases sean creadas a partir de las existentes y proporcionando una implementación específica.

Principio de sustitución de Liskov

Según el Principio de Sustitución de Liskov, las clases derivadas deben ser completamente sustituibles por sus clases base, lo que significa que las instancias de las clases derivadas deben poder usarse en lugar de instancias de las clases base sin alterar el comportamiento del programa.

En el siguiente ejemplo, no se cumple con el Principio de Sustitución de Liskov porque la clase derivada Triangle modifica el comportamiento del método virtual GetSides() de la clase base Rectangle. Al crear una instancia de Triangle y la asignas a una variable de tipo Rectangle, estás asumiendo que puedes utilizar esa instancia de la misma manera que si fuera una instancia de Rectangle, lo cual no es cierto en este caso. El método GetSides() de la clase Triangle devuelve «3», mientras que la clase Rectangle se supone que debe devolver «4».

public class Rectangle
{
    public virtual string GetSides()
    {
        return "4";
    }
}

public class Triangle : Rectangle
{
    public override string GetSides()
    {
        return "3";
    }
}

Rectangle rectangle = new Triangle();
Console.WriteLine(rectangle.GetSides()); // Muestra "3", debería mostrar "4"

En el siguiente ejemplo, se utiliza una interfaz IShape que define un método GetSides(). Tanto la clase Rectangle como la clase Triangle implementan esta interfaz y proporcionan su propia implementación para el método GetSides().

public interface IShape
{
    string GetSides();
}
public class Rectangle : IShape
{
    public string GetSides()
    {
        return "4";
    }
}
public class Triangle : IShape
{
    public string GetSides()
    {
        return "3";
    }
}

IShape shape = new Triangle();
Console.WriteLine(shape.GetSides()); // Muestra "3"
shape = new Rectangle();
Console.WriteLine(shape.GetSides()); // Muestra "4"

De esta manera, se puede utilizar la misma variable shape para hacer referencia a diferentes instancias de clases que implementan la interfaz IShape, lo que demuestra que todas las clases que implementan IShape tienen el mismo comportamiento para el método GetSides(). Por lo tanto, esta solución cumple el Principio de Sustitución de Liskov.

Principio de segregación de interfaces

En este ejemplo, la interfaz IAnimals proporciona métodos para comer, dormir, nadar y volar. Sin embargo, no todos los animales pueden volar. Por ejemplo, los peces no pueden volar. La clase Fish implementa la interfaz IAnimals, pero debe proporcionar una implementación vacía o lanzar una excepción para el método Fly(), lo que es inútil. Este ejemplo viola el principio de segregación de interfaz ya que la interfaz IAnimals es demasiado genérica y obliga a las clases que la implementan a proporcionar una funcionalidad inútil.

interface IAnimal
{
    void Eat();
    void Sleep();
    void Swim();
    void Fly();
}

class Fish : IAnimals
{
    public void Eat() { /* Código para comer */ }
    public void Sleep() { /* Código para dormir */ }
    public void Swim() { /* Código para nadar */ }
    public void Fly() { throw new NotImplementedException(); }
}

En el siguiente ejemplo, se crearon varias interfaces específicas para las acciones que los animales pueden realizar: IEat, ISleep, ISwim, IFly. La clase Fish implementa solo las interfaces IEat, ISleep y ISwim y la clase Bird implementa IEat, ISleep, IFly. Al utilizar interfaces específicas, se evita que las clases tengan que proporcionar una implementación inútil. Este ejemplo cumple con el principio de segregación de interfaz ya que las interfaces son lo suficientemente específicas y solo proporcionan la funcionalidad necesaria para las clases que las implementan. Esto permite que las clases sean más específicas y flexibles en términos de su funcionalidad.

interface IEat
{
    void Eat();
}

interface ISleep
{
    void Sleep();
}

interface ISwim
{
    void Swim();
}

interface IFly
{
    void Fly();
}

class Fish : IEat, ISleep, ISwim
{
    public void Eat() { /* Código para comer */ }
    public void Sleep() { /* Código para dormir */ }
    public void Swim() { /* Código para nadar */ }
}

class Bird : IEat, ISleep, IFly
{
    public void Eat() { /* Código para comer */ }
    public void Sleep() { /* Código para dormir */ }
    public void Fly() { /* Código para volar */ }
}

En resumen, el principio de segregación de interfaz se refiere a que una interfaz no debe proporcionar métodos innecesarios para sus implementadores, es decir, debe ser lo mas especifica posible para que solo se implementen los métodos que realmente necesita la clase. Al seguir este principio se evita tener interfaces con métodos irrelevantes para algunas clases y se aumenta la flexibilidad y reutilización del código.

Principio de inversión de dependencias

En este ejemplo, la clase Customer tiene una dependencia directa en la clase Product, ya que utiliza la clase Product directamente en su método Purchase(). Esto hace que la clase Customer sea inflexible y difícil de cambiar o probar. Si se desea cambiar el comportamiento de la clase Product, se tendría que modificar la clase Customer.

class Customer
{
    public void Purchase(Product product)
    {
        // Código para realizar la compra
    }
}

class Product
{
    public decimal Price { get; set; }
}

En el siguiente ejemplo, se utiliza una interfaz IPricing para abstraer la dependencia de la clase Customer en la clase Product. La clase Product implementa la interfaz IPricing y proporciona una implementación específica para el método GetPrice(). Esto hace que la clase Customer sea más flexible y fácil de cambiar o probar, ya que la dependencia se inyecta mediante la interfaz IPricing y no se tiene una dependencia directa en la clase Product.

interface IPricing
{
    decimal GetPrice();
}

class Customer
{
    public void Purchase(IPricing pricing)
    {
        decimal price = pricing.GetPrice();
        // Código para realizar la compra
    }
}

class Product : IPricing
{
    public decimal Price { get; set; }

    public decimal GetPrice()
    {
        return Price;
    }
}

Conclusión

En resumen, los principios SOLID son un conjunto de principios de diseño de software que buscan ayudar a los desarrolladores a crear código de alta calidad y fácil de mantener. Estos principios se enfocan en la responsabilidad única, la extensibilidad, la sustitución, la segregación de interfaces y la inversión de dependencias. Aplicar estos principios ayuda a asegurar que el código sea más fácil de entender, testear y escalar, y reducirá el riesgo de problemas en el futuro.

Espero que estos ejemplos te hayan ayudado a entender mejor los principios SOLID y cómo aplicarlos en tu código en C#😆

8
7
2
1