- Home
- .NET tutorials
- SOLID principles in C# used in object-oriented design
SOLID principles in C# used in object-oriented design
Published: Monday 19 September 2022
Using the SOLID principles in C# is important for the design of a .NET application.
Single-responsibility principle
The single-responsibility principle (or SRP) states that there should never be more than one reason for a class to change.
TeamService class:
public class Team {
}
public class TeamService {
public void Create(Team team) { }
public void Update(int id, Team team) { }
public void Delete(int id) { }
public void AddToCompetition(int teamId, int competitionId) { }
}
The Create, Update and Delete methods are fine in this instance, as they are directly related to the team.
AddToCompetition is related more to the competition then it is to the team.
CricketTeamService that include members specific for a cricket team, but still inherits the main TeamService. In this instance, we would also have to include the competition methods for both the TeamService and CricketTeamService.
public class Team {
}
public class TeamService {
public void Create(Team team) { }
public void Update(int id, Team team) { }
public void Delete(int id) { }
}
public class CompetitionService {
public void AddToCompetition(int teamId, int competitionId) { }
}
Open–closed principle
The open-closed principle (OCP) indicates that software entities should be open for extension but closed for modification.
Wheel. This class counts the total number of wheels on all the vehicles.
public class Car {
public int Wheels { get; set; }
}
public class Motorbike {
public int Wheels { get; set; }
}
public class Wheel {
public int CountTotalWheels(object[] vehicles) {
var wheelCount = 0;
foreach (var vehicle in vehicles) {
if (vehicle is Car) {
wheelCount += ((Car)vehicle).Wheels;
}
if (vehicle is Motorbike) {
wheelCount += ((Motorbike)vehicle).Wheels;
}
}
return wheelCount;
}
}
The good thing about this example is that this is meeting the first SOLID principle. The single-responsibility principle. The Wheel class is separate from the different vehicle classes.
CountTotalWheels method is not meeting the open-closed principle. It's not open for extension, as it can only handle cars and motorbikes without modification.
CountTotalWheels method would have to be modified.
Car and Motorbike have Wheels as properties. Let's move these properties to a Vehicle base class.
Car and Motorbike inherit from the Vehicle class, and as a result, it will be able to inherit these base properties.
Vehicle[] rather than type object[], making it type safe. In-addition, we can increment the wheel count, simply by calling the Wheels property.
public abstract class Vehicle {
public int Wheels { get; set; }
}
public class Car : Vehicle {
}
public class Motorbike : Vehicle {
}
public class Wheel {
public int CountTotalWheels(Vehicle[] vehicles) {
var wheelCount = 0;
foreach (var vehicle in vehicles) {
wheelCount += vehicle.Wheels;
}
return wheelCount;
}
}
At a later stage, if we want to count the wheels of a different vehicle, we could create a new class that inherits the Vehicle base class. If this is the case, the CountTotalWheels method would not have to be modified, as it's already passing in an instance of Vehicle as it's parameter.
Liskov substitution principle
Liskov substitution principle (LSP) is how a child class behaves when it inherits a parent class.
Car and Motorbike classes inherit from the Vehicle class.
public abstract class Vehicle {
public int Wheels { get; set; }
public virtual string? HelmetDesign { get; set; }
}
public class Car : Vehicle {
public override string? HelmetDesign => throw new NotImplementedException();
}
public class Motorbike : Vehicle {
}
In the Vehicle class, there is a property named HelmetDesign. That means that any class inheriting the Vehicle class must include it.
Motorbike class as there's a requirement to wear a helmet. However, an exception is thrown for the Car class as it's not required.
Car class is breaking the parent Vehicle class because it's not returning a string for the HelmetDesign property. Therefore, it's violating the LSP principle.
HelmetDesign property out of the Vehicle class, create a new IHelmet interface and add the property into there.
Motorbike class can implement IHelmet and include its members. As there is no requirement to wear a helmet in a car, the Car class just inherits the Vehicle class.
public abstract class Vehicle {
public int Wheels { get; set; }
}
public interface IHelmet {
string? HelmetDesign { get; set; }
}
public class Car : Vehicle {
}
public class Motorbike : Vehicle, IHelmet {
public string? HelmetDesign { get; set; }
}
Interface segregation principle
The interface segregation principle (ISP) is when a class implements an interface where it requires all its members.
ITeam interface which both the CricketTeam and PoolTeam classes implement has a BallCount and WicketCount property.
public interface ITeam {
int BallCount { get; set; }
int WicketCount { get; set; }
}
public class CricketTeam : ITeam {
public string? Name { get; set; }
public int BallCount { get; set; }
public int WicketCount { get; set; }
}
public class PoolTeam : ITeam {
public string? Name { get; set; }
public int BallCount { get; set; }
public int WicketCount { get; set; }
}
Both cricket and pool have at least one ball, so the BallCount property is valid. However, the game of pool does not have wickets. Therefore, the WicketCount is not needed for this.
PoolTeam class is inheriting an interface where it doesn't require all the members.
public interface ITeam
{
string? Name { get; set; }
public int BallCount { get; set; }
}
public interface IWicket
{
int WicketCount { get; set; }
}
public class CricketTeam : ITeam, IWicket
{
public string? Name { get; set; }
public int BallCount { get; set; }
public int GoalCount { get; set; }
public int WicketCount { get; set; }
}
public class PoolTeam : ITeam
{
public string? Name { get; set; }
public int BallCount { get; set; }
public int GoalCount { get; set; }
}
Dependency inversion principle
The dependency inversion principle (DIP) states that high-level classes should not depend on low-level classes.
public interface IRemoteControlService
{
public void PressOnButton();
}
public interface ITVService
{
public void TurnOn();
}
public class RemoteControlService : IRemoteControlService {
private readonly ITVService _tvService;
public RemoteControlService(ITVService tvService)
{
_tvService = tvService;
}
public void PressOnButton()
{
_tvService.TurnOn();
}
}
ASP.NET Core has its own IoC container, which can be used for dependency injection.
Watch the video
It is essential to understand the importance of the SOLID principles as a software developer.
Related tutorials
When does the try, catch and finally code blocks run in C#?
We have a look at a console app and ASP.NET Core app to see if the try, catch and finally statements always run in C# when an exception is thrown.