Table of contents
1.
Introduction
2.
What are SOLID Principles?
3.
The reason behind most unsuccessful applications
4.
Single Responsibility Principle (SRP)
4.1.
Initial Code
4.2.
Refactored Code
5.
Open/Closed Principle (OCP)
5.1.
Initial Code
5.2.
Refactored Code
6.
Liskov Substitution Principle (LSP)
6.1.
Initial Code
6.2.
Refactored Code
7.
Interface Segregation Principle (ISP)
7.1.
Initial Code
7.2.
Refactored Code
8.
Dependency Inversion Principle (DIP)
8.1.
Initial Code
8.2.
Refactored Code
9.
Why use SOLID Principles?
10.
Advantages of SOLID Principles in C#
11.
Disadvantages of SOLID Principles in C#
12.
Frequently Asked Questions
12.1.
Which languages use the SOLID principles?
12.2.
Is it necessary to follow all the SOLID principles?
12.3.
What are the SOLID principles in C#?
12.4.
What is solid principle in Web API? 
13.
Conclusion
Last Updated: Oct 3, 2024
Easy

SOLID Principles in C#

Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

Software development is a complex process and involves writing manageable and adaptable code. SOLID Principles are design principles that help us make our code more maintainable, scalable and flexible. These principles play a significant role in object-oriented languages like C#.

SOLID Principle C#

This article will look closely at all SOLID Principles in C# and see how they can help us write better code in C#. We will also look at code examples before and after applying each SOLID principle to understand them better.

What are SOLID Principles?

In C#, SOLID design concepts are fundamental design principles. SOLID Principles help us win the majority of design issues. These ideas were developed by Robert Martin in the 1990s. These ideas demonstrated many techniques for dealing with least encapsulation and tightly connected programming. Generally, depending on their experience, programmers begin the process of application development with clean and good designs. However, programs may develop faults over time, and the application must be changed for each new feature or change request.

The 5 SOLID Principles are

  1. S - Single Responsibility Principle
  2. O - Open/Closed Principle
  3. L - Liskov Substitution Principle
  4. I - Interface Segregation Principle
  5. D - Dependency Inversion Principle
     

We can use these principles to make a modular code base, making adding features easier. Such a design promotes code reuse and reduces coupling between different classes.

The reason behind most unsuccessful applications

When developers fail to follow the below-mentioned principles, it can lead to several issues in the application, which makes them unsuccessful. Let's see how violating each principle can contribute to an application's failure:

1. Single Responsibility Principle (SRP): If classes or modules in an application have multiple responsibilities or reasons to change, it violates the SRP. This leads to tight coupling, reduced maintainability, and increased complexity. When changes are required in one responsibility, it can inadvertently affect other unrelated parts of the application, making it prone to bugs and difficult to modify.

2. Open-Closed Principle (OCP): Violating the OCP means that the application's modules or classes are not open for extension but closed for modification. This results in rigid and inflexible code that is hard to extend or adapt to new requirements. When new functionality needs to be added, developers often resort to modifying existing code, introducing the risk of breaking existing functionality and making the application unstable.

3. Liskov Substitution Principle (LSP): If the LSP is not followed, it means that derived classes do not properly substitute their base classes. This can lead to unexpected behavior and incorrect results when using inheritance. When the application relies on the behavior of base classes but the derived classes violate the contract, it can cause runtime errors, inconsistencies, and make the application unreliable.

4. Interface Segregation Principle (ISP): Violating the ISP means that interfaces are not properly segregated based on the client's needs. This results in bloated interfaces with methods that clients are forced to implement but don't actually use. It leads to unnecessary dependencies, increased complexity, and reduced maintainability. Clients become coupled to methods they don't need, making the application harder to understand and modify.

5. Dependency Inversion Principle (DIP): If the DIP is not adhered to, it means that high-level modules directly depend on low-level modules, creating tight coupling. This makes the application rigid and difficult to change. When modifications are made to low-level modules, it can have a cascading effect on the high-level modules, requiring extensive changes throughout the application. It hinders the ability to replace or update dependencies without impacting the entire system.

Single Responsibility Principle (SRP)

The Single Responsibility Principle states that any class in an object-oriented design should perform only one function. Thus such a class will change only when its function changes. All elements inside a class should perform one function collectively. 

A class with only one function is easier to understand, test and modify. On the other hand, when a class performs multiple functions, it becomes more complex, leading to a tightly coupled design that is hard to test and prone to bugs.

SRP also helps in code reuse as we can use a class with only one responsibility in different places. In contrast, we can use a class with multiple responsibilities only at certain places.

Let us look at an example of using the SRP.

Initial Code

public class Car {
	public void StartEngine() {
		// Code to start the engine
	}
    
	public void Drive() {
		// Code to drive the car
	}
    
	public void StopEngine() {
		// Code to stop the engine
	}
    
	public void PlayMusic() {
		// Code to play music
	}
}

Refactored Code

public class Car {
	public void StartEngine() {
		// Code to start the engine
	}
    
	public void Drive() {
		// Code to drive the car
	}
    
	public void StopEngine() {
		// Code to stop the engine
	}
}

public class MusicPlayer {
	public void PlayMusic() {
		// Code to play music
	}
}

 

Explanation

Initially, the Car class has multiple responsibilities like starting the engine, driving the car and stopping the engine. The Car class is also responsible for playing music, which violates SRP.

We refactor this code and split the Car class into two classes - Car and MusicPlayer. Each class is responsible for only one functionality, thus following SRP.

Open/Closed Principle (OCP)

The Open/Closed Principle states that a class should be open for extension but closed for modification, which means that the design of a class should allow us to add new functionality without changing the old code. This principle promotes the use of polymorphism and inheritance.

When we follow OCP, we ensure that we do not change the old code, which means we cannot break any old functionality. We create a new derived class if we want to add some features.

Let us look at an example of using the OCP.

Initial Code

public class Shape {
	public string Type { get; set; }
	public double Height { get; set; }
	public double Width { get; set; }

	public double CalculateArea() {
		if (Type == "Rectangle") {
			return Height * Width;
		}
		else if (Type == "Circle") {
			return Math.PI * (Width / 2) * (Width / 2);
		}
		else {
			throw new NotSupportedException();
		}
	}
}

Refactored Code

public abstract class Shape {
	public double Height { get; set; }
	public double Width { get; set; }

	public abstract double CalculateArea();
}

public class Rectangle : Shape {
	public override double CalculateArea() {
		return Height * Width;
	}
}

public class Circle : Shape {
	public override double CalculateArea() {
		return Math.PI * (Width / 2) * (Width / 2);
	}
}

 

Explanation

Initially, the Shape class violates OCP as it has a switch statement to determine the type of shape. So if we want to add a new shape, this switch statement inside the CalculateArea function will need another case.

In the refactored code, we make an abstract Shape class. We make two subclasses from the Shape class and override the CalculateArea abstract method. Now if we want to add a new shape, we do not need to modify any existing code.

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that we should be able to replace objects of a superclass with objects of its subclasses without causing any issues or unexpected behaviour.

It ensures that subclasses do not break the functionality of their superclass. We expect subclasses to behave similarly to the superclass.

Let us look at an example of using the LSP.

Initial Code

public class Rectangle {
	public virtual int Width { get; set; }
	public virtual int Height { get; set; }

	public int GetArea() {
		return Width * Height;
	}
}

public class Square : Rectangle {
	public override int Width {
		get { return base.Width; }
		set {
			base.Width = value;
			base.Height = value;
		}
	}

	public override int Height {
		get { return base.Height; }
		set {
			base.Height = value;
			base.Width = value;
		}
	}
}

Refactored Code

public abstract class Shape {
	public abstract int GetArea();
}

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

	public override int GetArea() {
		return Width * Height;
	}
}

public class Square : Shape {
	public int SideLength { get; set; }

	public override int GetArea() {
		return SideLength * SideLength;
	}
}


Explanation

Initially, the Square class inherits from the Rectangle class and overrides both height and width properties such that they are always the same. If we try to assign a Square object to a Rectangle, we can set the Height and Width to different values, but when we calculate the area of the Rectangle object, we get the wrong values.

In the refactored code, the Shape class defines an abstract method GetArea. Now we can replace a Rectangle object with a Square object and still get the correct result.

Interface Segregation Principle (ISP)

The Interface Segregation Principle states that any class should only implement the necessary interfaces/methods it uses to achieve its final goal. An interface is a set of methods, properties etc. that a class defines without revealing implementation details.

It reduces coupling between classes as each class depends on its relevant interfaces. It also increases the cohesion between different elements in a module.

Let us look at an example of using the ISP.

Initial Code

public interface IWorker {
	void Work();
	void Eat();
	void Sleep();
}

public class Worker : IWorker {
	public void Work() {
		// Code to work
	}

	public void Eat() {
		// Code to eat
	}

	public void Sleep() {
		// Code to sleep
	}
}

public class Robot : IWorker {
	public void Work() {
		// Code to work
	}

	public void Eat() {
		// Do nothing (Robots do not eat)
	}

	public void Sleep() {
		// Do nothing (Robots do not sleep)
	}
}

Refactored Code

public interface IWorkable {
	void Work();
}

public interface IEatable {
	void Eat();
}

public interface ISleepable {
	void Sleep();
}

public class Worker : IWorkable, IEatable, ISleepable {
	public void Work() {
		// Code to work
	}

	public void Eat() {
		// Code to eat
	}

	public void Sleep() {
		// Code to sleep
	}
}

public class Robot : IWorkable {
	public void Work() {
		// Code to work
	}
}

 

Explanation

Initially, the Worker class implements all three methods of the interface IWorker. But the Robot class does not need to implement Eat or Sleep, as robots do not eat or sleep. Thus initially, this code violates ISP as it forces the Robot class to implement these methods.

We refactor this code and make separate interfaces for working, eating, and sleeping. The Worker class implements all three interfaces, whereas the Robot class implements only the interface related to work.

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should not directly depend on low-level modules. Instead, both implementations should depend on abstractions (free of underlying implementation details).

If we use always use interfaces, we can modify the implementation of low-level modules without changing the interface and the high-level modules using that interface. Thus, DIP reduces coupling between modules.

Let us look at an example of using DIP.

Initial Code

public class UserService  {
	private UserRepository _userRepository = new UserRepository();

	public void AddUser(User user) {
		_userRepository.Add(user);
	}
}

public class UserRepository {
	public void Add(User user) {
		// Code to add a user to the database
	}
}

Refactored Code

public interface IUserRepository {
	void Add(User user);
}

public class UserService  {
	private readonly IUserRepository _userRepository;

	public UserService(IUserRepository userRepository) {
		_userRepository = userRepository;
	}

	public void AddUser(User user) {
		_userRepository.Add(user);
	}
}

 

Explanation

Initially, the UserService class directly depends on UserRepository class, which increases their coupling. If we change the UserRepository class, the UserService class will also need changes.

In the refactored code, we use an IUserRepository interface which defines behaviour for adding a user to a repository. The UserService class only sees an interface but does not see the implementation details, promoting abstraction.

Why use SOLID Principles?

The various reasons to use solid principles are:

  • Because SOLID principles encourage the production of modular, adaptable code that is less prone to errors and more resistant to changes in requirements, you can create code that is easier to maintain and adjust over time.
     
  • As they encourage the use of abstractions and separated dependencies, the SOLID principles can help to make software more scalable, preventing the codebase from becoming unduly complicated and difficult to manage.
     
  • These principles support the development of adaptable code that is open to expansion but closed to alteration, allowing for flexibility without compromising existing functionality.
     
  • By following these guidelines, you can develop components that are easier to reuse in other parts of your program.

Advantages of SOLID Principles in C#

There are many benefits of using the SOLID Principles.

  1. These principles promote modular design, which makes code more readable and maintainable.
     
  2. These principles create flexible code, which changes as the requirements change.
     
  3. These principles encourage code reuse.
     
  4. These principles make code easier to test and debug.

Disadvantages of SOLID Principles in C#

  • SOLID principles can result in a more complex codebase because they demand breaking code into smaller, more focused classes and interfaces.
     
  • Following SOLID principles may result in the creation of more classes and interfaces, making the codebase more difficult to browse.
     
  • SOLID principles are difficult to grasp and apply, particularly for junior engineers or developers new to software development.
     
  • Following SOLID principles takes more time and effort since it necessitates extensive planning and design before you begin creating code.

Frequently Asked Questions

Which languages use the SOLID principles?

SOLID concepts are applicable across languages, providing guidance in both object-oriented and non-object-oriented programming approaches. Java, Python, and Ruby are among prominent languages where they are used to improve code maintainability and flexibility.

Is it necessary to follow all the SOLID principles?

In the projects, it is good to follow all SOLID principles. We appreciate their use since it provides important principles for enhancing overall code quality, ensuring maintainability and scalability, and ultimately contributing to robust and efficient software solutions.

What are the SOLID principles in C#?

In C#, SOLID design concepts are fundamental design principles. SOLID Principles help us win the majority of design issues. These ideas were developed by Robert Martin in the 1990s. These ideas demonstrated many techniques for dealing with least encapsulation and tightly connected programming.

What is solid principle in Web API? 

The SOLID principles in Web API design refer to a set of five design principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion) that promote modular, decoupled, and flexible design in order to create robust, maintainable, and extensible API code.

Conclusion

This article explored the SOLID principles in C#. These principles are effective in improving the software quality. By mastering these principles, we can build robust and scalable applications that are easier to maintain and modify.

We recommend reading the following articles to learn more about CS fundamentals:

If you liked our article, do upvote our article and help other ninjas grow.

Live masterclass