Trusted answers to developer questions

What are the SOLID principles in C#?

Get Started With Data Science

Learn the fundamentals of Data Science with this free course. Future-proof your career by adding Data Science skills to your toolkit — or prepare to land a job in AI, Machine Learning, or Data Analysis.

SOLID is an acronym for the five object-oriented design principles. These principles enable developers to develop software that is easily extendable and maintainable. SOLID principles also ensure that software can easily adapt to changing requirements. Let’s look at each of the principles, one by one:

svg viewer

Single responsibility principle​ (SRP)

SRP states that every class must have only one reason to change it. In other words, each class must be responsible for a single purpose only. Consider the following code snippet, featuring a User class with an update method:

class User
{
void update(Database db, string msg)
{
try
{
db.Add(msg); // Update the DB
}
catch (Exception ex) // Catch and log error.
{
File.WriteAllText("errors.txt", ex.ToString());
}
}
}

The User class is not only updating the database, ​its also handling logging of errors.

Consider the SRP compliant code below:

class User
{
private ErrorLogger error = new ErrorLogger();
void update(Database db, string msg)
{
try
{
db.Add(msg);
}
catch (Exception ex)
{
error.handle(ex.ToString());
}
}
}
class ErrorLogger
{
void handle(string error)
{
File.WriteAllText("errors.txt", error);
}
}

Open closed principle (OCP)

OCP states that classes must be open for extension, but closed for modification:

class TextMessage
{
void sendMsg(Database db, string msg)
{
if (msg.StartsWith("@"))
{
db.Tag(msg); // messages starting with @ are tags.
}
// if another message that starts with '#' needs,
// to be handled, then another if condition will be needed.
// The class will need to be heavily modified to
// incorporate changing requirements.
else
{
db.Add(msg); // otherwise they're just simple messages.
}
}
}

In the above snippet, the TextMessage class has a sendMsg module. This module behaves differently when the message starts with @.

However, if another type of message, that starts with # needs to be handled, then the class will need to be modified by adding another if statement.

Consider the code below; it uses inheritance to comply with the Open-Closed Principle:

class TextMessage
{
void sendMsg(Database db, string msg)
{
db.Add(msg);
}
}
// A class made specifically for tagging.
// If a new feature needs to be incorporated
class Tag : TextMessage // class Tag inherits TextMessage
{
override void sendMsg(Database db, string msg)
{
db.Tag(msg);
}
}

Liskov substitution principle​ (LSP)

LSP states that if SS is a subtype of TT, then objects of type TT may be replaced with objects of type SS. Consider the example below:

class Bird
{
void fly()
{
System.Console.WriteLine("I believe I can fly!");
}
}
class Dove : Bird {}
class Ostrich : Bird {}

The Dove class is a subtype of Bird; objects of Bird can be replaced by objects of Dove because doves can fly. However, the same cannot be done with objects of Ostrich since they cannot fly. This is a violation of LSP.

Now consider the LSP compliant code below:

class Bird
{
// functions that all birds do
}
class FlyingBirds : Bird
{
void fly()
{
System.Console.WriteLine("I believe I can fly!");
}
}
class Dove : FlyingBirds {}
class Ostrich : Bird {}

Objects of FlyingBirds are replaceable by Dove, and objects of simple Bird are replaceable by objects of Ostrich; hence the code follows LSP.

Interface segregation principle (ISP)

ISP suggests that several specific client interfaces are better than having one general interface. In other words, it is better to have multiple interfaces instead of adding lots of functionality to one interface.

Consider the example below:

interface phoneTasks
{
void call();
void text();
void games();
void torch();
}
class smartPhone : phoneTasks
{
public void call(){}
public void text(){}
public void games(){}
public void torch(){}
}
class dumbPhone: phoneTasks
{
public void call(){}
public void text(){}
// But the dumb phone does not have games, or a torch.
// This is an ISP violation.
public void games(){}
public void torch(){}
}

The interface phoneTasks has methods for tasks that a smartphone can perform; however, another phone may not have all of these features​ but is still implementing them.

Now consider the ISP compliant code below:

// Lots of specific interfaces is better than
// one big general interface.
interface phoneTasks
{
void call();
void text();
}
interface Games
{
void games();
}
interface Torch
{
void torch();
}
class smartPhone : phoneTasks, Games, Torch
{
public void call(){}
public void text(){}
public void games(){}
public void torch(){}
}
// dumbPhone is only implementing the features it has.
// no unneccessary interfaces.
class dumbPhone: phoneTasks
{
public void call(){}
public void text(){}
}

Dependency inversion principle (DIP)

DIP states that:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

Consider an example of a calculator class that is dependent on two low-level classes that define its features. Now, if more features are to be added, then the​ calculator class would need to be modified.

DIP dictates that high-level classes must not be dependent on low-level classes. In this case, the calculator class should not be bogged down with the details of the implementation of its features. The illustration below highlights this idea:

The calculator class is a high level module that has two low level classes for addition and subtraction.
1 of 4

RELATED TAGS

solid
principles
c#
Copyright ©2024 Educative, Inc. All rights reserved
Did you find this helpful?