SOLID

5 Examples SOLID Principles in C#

SOLID is an acronym for five principles of object-oriented software design, which are:

  1. Single responsibility principle: Each class should have a single responsibility, and that responsibility should be fully encapsulated by the class.
  2. Open-closed principle: Classes must be designed in such a way that they are easily extensible without modifying their source code.
  3. Liskov substitution principle: Derived classes must be substitutable for their base classes.
  4. Interface segregation principle: Classes should not be forced to implement interfaces that they do not use.
  5. Dependency inversion principle: Classes should depend on abstractions, not concrete implementations.

Single responsibility principle

In this example, the class Customer has two responsibilities: to check if the customer is valid and to save the customer to the database. In a SOLID design, these two responsibilities should be in different classes, to comply with the single responsibility principle. For example, one class CustomerValidator for validation and another class CustomerRepository for persistence in the database.

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

    public bool Validate()
    {
        // Valid if the customer is valid
    }

    public void Save()
    {
        // Save the customer in the database
    }
}

In the following example, each class has a single responsibility: the class CustomerValidator is in charge of validating if the client is valid, the class CustomerRepository is in charge of saving the client in the database, and the class Customer contains the information of the client. In this way, the principle of single responsibility is fulfilled since each class only has a specific responsibility.

class CustomerValidator
{
    public bool Validate(Customer customer)
    {
        // Valid if the client is valid
    }
}

class CustomerRepository
{
    public void Save(Customer customer)
    {
        // Save the customer in the database
    }
}

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

Open-closed principle

In this example, the class Shape has a method to calculate the area, but it uses an if-else control flow structure to determine the shape type and calculate the area differently. If you want to add a new shape, you must modify the code of the class Shape violating the open-closed principle.

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; }
}

In the following example, the class Shape is an abstract class that has an abstract method to calculate the area. The classes Rectangle and Circle inherit from Shape and provide specific implementations for calculating area. With this structure, we can add new shapes (such as a triangle or a circle) without modifying the code of the class Shape, adhering to the open-closed principle.

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;
    }
}

In summary, the open-closed principle refers to the fact that classes should be designed in such a way that they are easily extensible without modifying their source code. One way to achieve this is through abstraction and inheritance, allowing new classes to be created from existing ones and providing a specific implementation.

Liskov substitution principle

According to the Liskov Substitution Principle, derived classes must be fully replaceable by their base classes, which means that instances of derived classes must be able to be used in place of instances of base classes without altering the behavior of the program.

In the following example, the Liskov Substitution Principle is not satisfied because the derived class Triangle modifies the behavior of the virtual method GetSides()of the base class Rectangle. By creating an instance of Triangle and assigning it to a variable of type Rectangle, you are assuming that you can use that instance in the same way as if it were an instance of Rectangle, which is not true in this case. The method GetSides() of the class Triangle returns “3”, while the class Rectangle is supposed to return “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()); // Show "3", should show "4"

In the following example, an interface is used IShape that defines a method GetSides(). Both class Rectangle and class Triangle implement this interface and provide their own implementation for the 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()); // Show "3"
shape = new Rectangle();
Console.WriteLine(shape.GetSides()); // Show "4"

In this way, the same variable can be used shape to reference different instances of classes that implement the interface IShape, proving that all implementing classes IShape have the same behavior for the method GetSides(). Therefore, this solution satisfies the Liskov Substitution Principle.

Interface segregation principle

In this example, the interface IAnimals provides methods for eating, sleeping, swimming, and flying. However, not all animals can fly. For example, fish cannot fly. The class Fish implements the interface IAnimals, but you must provide an empty implementation or throw an exception for the method Fly(), which is useless. This example violates the principle of interface segregation since the interface IAnimals is too generic and forces the classes that implement it to provide useless functionality.

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

class Fish : IAnimals
{
    public void Eat() { /* code to eat */ }
    public void Sleep() { /* sleeping code */ }
    public void Swim() { /* code to swim */ }
    public void Fly() { throw new NotImplementedException(); }
}

In the following example, several interfaces have been created specifically for the actions that animals can perform: IEatISleepISwimIFly. The class Fish implements only the IEatISleep and ISwim interfaces and the class Bird implements IEatISleepIFly. By using specific interfaces, you avoid classes having to provide a useless implementation. This example complies with the principle of interface segregation since interfaces are specific enough and only provide the necessary functionality for the classes that implement them. This allows classes to be more specific and flexible in terms of their functionality.

interface IEat
{
    void Eat();
}

interface ISleep
{
    void Sleep();
}

interface ISwim
{
    void Swim();
}

interface IFly
{
    void Fly();
}

class Fish : IEat, ISleep, ISwim
{
    public void Eat() { /* code to eat */ }
    public void Sleep() { /* sleeping code */ }
    public void Swim() { /* code to swim */ }
}

class Bird : IEat, ISleep, IFly
{
    public void Eat() { /* code to eat */ }
    public void Sleep() { /* sleeping code */ }
    public void Fly() { /* code to fly */ }
}

In summary, the principle of interface segregation refers to the fact that an interface must not provide unnecessary methods for its implementers, that is, it must be as specific as possible so that only the methods that the class really needs are implemented. Following this principle avoids having interfaces with irrelevant methods for some classes and increases flexibility and code reuse.

Dependency Inversion Principle

In this example, the class Customer has a direct dependency on the class Product, since it uses the class Product directly in its method Purchase(). This makes the class Customer inflexible and difficult to change or test. If you want to change the behavior of the class Product, you would have to modify the class Customer.

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

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

In the following example, an interface is used IPricing to abstract the class’s dependency Customer onto class Product. The class Product implements the interface IPricingand provides a specific implementation for the method GetPrice(). This makes the class Customer more flexible and easier to change or test, since the dependency is injected via the interface IPricing and you don’t have a direct dependency on the class Product.

interface IPricing
{
    decimal GetPrice();
}

class Customer
{
    public void Purchase(IPricing pricing)
    {
        decimal price = pricing.GetPrice();
        // Code to make the purchase
    }
}

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

    public decimal GetPrice()
    {
        return Price;
    }
}

Conclusion

In short, SOLID principles are a set of software design principles that seek to help developers create high-quality, maintainable code. These principles focus on single responsibility, extensibility, substitution, interface segregation, and dependency inversion. Applying these principles helps ensure that code is easier to understand, test, and scale, and will reduce the risk of problems in the future.

I hope these examples have helped you better understand SOLID principles and how to apply them to your C# code😆

1