Introduction: SOLID is an acronym representing five principles that assist developers in building maintainable and scalable systems. In this tutorial, we will explore the SOLID principles with practical C# examples.
S - Single Responsibility Principle (SRP): SRP states that a class should have only one reason to change. This ensures that a class is focused on what it should do and does not take on responsibilities that it shouldn't.
Example:
public class Logger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
public class Calculator
{
private readonly Logger _logger;
public Calculator(Logger logger)
{
_logger = logger;
}
public int Add(int a, int b)
{
_logger.Log("Adding numbers.");
return a + b;
}
}
Here, the Calculator
class is only concerned with calculations and the Logger
the class handles logging.
O - Open/Closed Principle (OCP): OCP states that a class should be open for extension but closed for modification. This can be achieved by using interfaces, abstract classes, or virtual methods.
Example:
public interface IShape
{
double Area();
}
public class Rectangle : IShape
{
public double Width { get; set; }
public double Height { get; set; }
public double Area()
{
return Width * Height;
}
}
public class Circle : IShape
{
public double Radius { get; set; }
public double Area()
{
return Math.PI * Radius * Radius;
}
}
L - Liskov Substitution Principle (LSP): The Liskov Substitution Principle (LSP) states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. In simple terms, if class B is a subclass of class A, then we should be able to replace A with B without disrupting the behavior of the program.
One common scenario where LSP is violated involves overriding the base class methods in a way that's not compatible with the behavior expected from the base class.
Example: Let's consider an example involving shapes:
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area()
{
return Width * Height;
}
}
public class Square : Rectangle
{
private int _side;
public override int Width
{
get { return _side; }
set { _side = value; }
}
public override int Height
{
get { return _side; }
set { _side = value; }
}
}
This example involves a Rectangle
class and a Square
class that inherits from Rectangle
. It seems logical, as a square is a special kind of rectangle. However, this violates the Liskov Substitution Principle. A square has all sides equal, so setting either the width or the height should set both, while a rectangle doesn't have this constraint.
If you use a Square
object in a context where a Rectangle
object is expected, this could lead to unexpected behavior.
csharpCopy codeRectangle rectangle = new Square();
rectangle.Width = 5;
rectangle.Height = 10;
Console.WriteLine(rectangle.Area()); // This will output 100 instead of 50.
Solution: To comply with LSP, we could use a more general type for both classes, like Polygon
, and then use separate properties and methods that make sense for each derived class without making assumptions.
public interface IPolygon
{
int Area();
}
public class Rectangle : IPolygon
{
public int Width { get; set; }
public int Height { get; set; }
public int Area()
{
return Width * Height;
}
}
public class Square : IPolygon
{
public int Side { get; set; }
public int Area()
{
return Side * Side;
}
}
In this example, both Rectangle
and Square
implement the IPolygon
interface. This approach respects the Liskov Substitution Principle and avoids unexpected behavior.
I - Interface Segregation Principle (ISP): ISP states that a class should not be forced to implement interfaces it doesn't use. Interfaces should be small and focused.
Example:
public interface IPrinter
{
void Print();
}
public interface IFax
{
void Fax();
}
public class Printer : IPrinter
{
public void Print()
{
// print logic
}
}
D - Dependency Inversion Principle (DIP): DIP encourages dependence on abstractions rather than concretions. This makes the system more flexible and adaptive to change.
Example:
public interface IMessageSender
{
void SendMessage(string message);
}
public class EmailSender : IMessageSender
{
public void SendMessage(string message)
{
// Send email
}
}
public class Notification
{
private readonly IMessageSender _messageSender;
public Notification(IMessageSender messageSender)
{
_messageSender = messageSender;
}
public void Notify(string message)
{
_messageSender.SendMessage(message);
}
}
Conclusion: The SOLID principles are essential for creating maintainable, scalable, and robust systems. With practical application in C#, you can write cleaner and more efficient code that stands the test of time.