20 min read

Understanding and Implementing S.O.L.I.D Principles in C#

#dotnet

Software development is an intricate field that constantly evolves with new methodologies and practices. One of the core concepts for developing robust and maintainable software is the S.O.L.I.D principles. These principles, introduced by Robert C. Martin, are a subset of many principles promoted by object-oriented design and programming. Let’s dive into these principles with examples in C#, specifically in the context of .NET Core.

1. Single Responsibility Principle (SRP)

Concept:

The Single Responsibility Principle dictates that a class should have one, and only one, reason to change. This means a class should only have one job or responsibility.

Example in C#:


    public class UserService
    {
        public void AddUser(User user)
        {
            // Code to add user
        }

        public User GetUser(int userId)
        {
            // Code to get a user
            return new User();
        }
    }

    public class Logger
    {
        public void Log(string message)
        {
            // Code to log message
        }
    }

In this example, UserService is responsible only for user-related operations, while Logger takes care of logging activities.

Pros and Cons:

Pros:
Easier to maintain and test.
Reduced impact of changes.

Cons:
Might lead to an increase in the number of classes.

2. Open/Closed Principle (OCP)

Concept:

Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This principle aims to allow the behavior of a module to be extended without modifying its source code.

Example in C#:


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

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

        public override double Area()
        {
            return Math.PI * Radius * Radius;
        }
    }

    public class Square : Shape
    {
        public double Side { get; set; }

        public override double Area()
        {
            return Side * Side;
        }
    }

Here, Shape is closed for modification but open for extension to allow different shapes like Circle and Square.

Pros and Cons:

Pros:
Promotes reusable code.
Enhances modularity.
Cons:
Initial complexity in setting up the structure.

3. Liskov Substitution Principle (LSP)

Concept:

Subtypes must be substitutable for their base types without altering the correctness of the program.

Example in C#:


    public abstract class Bird
    {
        public abstract void Fly();
    }

    public class Sparrow : Bird
    {
        public override void Fly()
        {
            // Implementation for flying
        }
    }

    public class Ostrich : Bird
    {
        public override void Fly()
        {
            throw new NotImplementedException("Ostriches can't fly");
        }
    }

In this example, substituting Sparrow with Ostrich might lead to issues, indicating a violation of LSP.

Pros and Cons:

Pros:
Increases robustness of the code.
Cons:
Sometimes restrictive in certain scenarios.

4. Interface Segregation Principle (ISP)

Concept:

Clients should not be forced to depend on interfaces they do not use. This principle encourages creating specific interfaces rather than one general-purpose interface.

Example in C#:


    public interface IUserReadOperations
    {
        User GetUser(int userId);
    }

    public interface IUserWriteOperations
    {
        void AddUser(User user);
    }

    public class UserService : IUserReadOperations, IUserWriteOperations
    {
        public User GetUser(int userId)
        {
            // Get user logic
            return new User();
        }

        public void AddUser(User user)
        {
            // Add user logic
        }
    }

UserService implements two interfaces, segreg

ating reading and writing operations.

Pros and Cons:

Pros:
Improved organization and clarity of code.
Reduces the impact of changes.
Cons:
Increased number of interfaces, potentially complicating the design.

5. Dependency Inversion Principle (DIP)

Concept:

High-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.

Example in C#:


    public interface IDatabase
    {
        void Save(object data);
    }

    public class SqlDatabase : IDatabase
    {
        public void Save(object data)
        {
            // SQL save logic
        }
    }

    public class UserService
    {
        private IDatabase _database;

        public UserService(IDatabase database)
        {
            _database = database;
        }

        public void AddUser(User user)
        {
            _database.Save(user);
        }
    }

In this setup, UserService depends on the IDatabase interface, not on the concrete implementation, allowing for greater flexibility and decoupling.

Pros and Cons:

Pros:
Promotes flexibility and scalability.
Reduces coupling between high-level and low-level modules.
Cons:
Can add complexity to the codebase.

Conclusion

The S.O.L.I.D principles provide a guideline for designing software that is easy to maintain, extend, and refactor. While they offer numerous benefits, it’s crucial to understand the context and requirements of your project. Blindly applying these principles can sometimes lead to over-engineering. However, when used judiciously, they can significantly improve the quality of your software, especially in complex systems like those built with .NET Core. As with any design principle, balance and pragmatism are key.

Support ❀️
If you have enjoyed my content and code, do support me by buying a couple of coffees. This will enable me to dedicate more time to research and create new content. Cheers!
Share this Article
Share this article with your network to help others!
What's your Feedback?
Do let me know your thoughts around this article.