In object-oriented computer programming, SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable.
Initial | Acronyms | Concept |
---|---|---|
S | SRP | Single-responsibility principle A class should have one and only one reason to change, meaning that a class should have only one job. |
O | OCP | Open–closed principle Objects or entities should be open for extension, but closed for modification. |
L | LSP | Liskov substitution principle Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T. |
I | ISP | Interface segregation principle A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use. |
D | DIP | Dependency inversion principle Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions. |
Well, once we have seen the definition and its concepts, let's develop a practical example to better understand
Think of a fairly ... fairly simple application that calculates the sum of the areas and perimeters of a set of rectangles. For this, you have a class called Rectangle
which looks something like this: see full code
public class Rectangle
{
public double Sides { get; } = 4;
public double Height { get; set; }
public double Width { get; set; }
public static double SumAreas(IEnumerable<Rectangle> rectangles)
{
//
}
public static double SumPerimeters(IEnumerable<Rectangle> rectangles)
{
//
}
}
While the main program is in charge of creating the rectangles
and calling the respective methods to obtain the sums: see full code
var rectangles = new[]
{
new Rectangle {Width = 10, Height = 5},
new Rectangle {Width = 4, Height = 6},
new Rectangle {Width = 5, Height = 1},
new Rectangle {Width = 8, Height = 9}
};
var sumAreas = Rectangle.SumAreas(rectangles);
var sumPerimeters = Rectangle.SumPerimeters(rectangles);
Everything seems perfect, your program works like a charm, but it is violating the principle of single responsibility.
The violation occurs when declaring the SumAreas
and SumPerimeters
methods within the same class as Rectangle
and is that although they are related to the rectangle as such, the summation is part of our application logic , not the logic that a rectangle could have in real life.
To comply with the principle, we remove the summation functionality of the Rectangle
class and introduce a couple of classes in charge of performing the operations on the sets of rectangles, their code is more or less this: see full code
public class AreaOperation
{
public static double Sum(IEnumerable<Rectangle> rectangles)
{
//
}
}
public class PerimeterOperation
{
public static double Sum(IEnumerable<Rectangle> rectangles)
{
//
}
}
So each class has a single responsibility: one represents a rectangle and the others are responsible for doing operations related to them. Now, suppose that after a certain time, people liked your program so much that they ask you to also take into account equilateral triangles and that your program allows adding the areas of triangles with rectangles.
So you create a new class to represent the triangles, and you modify the methods to add areas so that they accept both rectangles and triangles and inside them you check what type each object is and perform the appropriate calculation: see full code
public class AreaOperation
{
public static double Sum(IEnumerable<object> shapes)
{
double area = 0;
foreach (var shape in shapes)
{
switch (shape)
{
case Rectangle rectangle:
area += rectangle.Height * rectangle.Width;
break;
case EquilateralTriangle triangle:
area += Math.Sqrt(3) * Math.Pow(triangle.SideLength, 2) / 4;
break;
}
}
return area;
}
}
public class PerimeterOperation
{
public static double Sum(IEnumerable<object> shapes)
{
double perimeter = 0;
foreach (var shape in shapes)
{
switch (shape)
{
case Rectangle rectangle:
perimeter += 2 * rectangle.Height + 2 * rectangle.Width;
break;
case EquilateralTriangle triangle:
perimeter += triangle.SideLength * 3;
break;
}
}
return perimeter;
}
}
Everything seems perfect again, your program works like a charm, but it is violating the open/closed principle.
You probably already have an idea of where in the code this principle is being violated ... but if not: in the classes of operations, and that is that your program is open to the extension ... but not to the modification. Think about what will happen tomorrow that circles become fashionable. You would have to modify the code of the operations so that it works with another figure and thus for each figure that occurs to the users of your program.
The solution to this violation will be given through the use of abstractions (in this case the IGeometricShape
interface) through which we will indicate that our figures share properties and methods (in this case the area and the perimeter): see full code
public interface IGeometricShape
{
double Area();
double Perimeter();
}
And we also have to modify the operations classes with the intention that it can accept object that comply with that behavior, in order to call those methods, without having to worry about what type "really" the objects are working with: see full code
public double Sum(IEnumerable<IGeometricShape> shapes)
{
double area = 0;
foreach (var shape in shapes)
area += shape.Area();
return area;
}
public double Sum(IEnumerable<IGeometricShape> shapes)
{
double perimeter = 0;
foreach (var shape in shapes)
perimeter += shape.Perimeter();
return perimeter;
}
In this way, when we add new figures in the future, we will only have to implement this behavior in common and we will not have to modify the existing code.
Now people start asking your program to calculate the information of square shapes, so you create a class called Square
which inherits from the Rectangle
class you created a few steps ago, after all, a square is no more than a rectangle with a small constraint. And to meet that constraint, you changed the behavior of its Height
and Width
properties: see full code
public class Square : Rectangle
{
private double _height;
private double _width;
public override double Height
{
get => _height;
set
{
_height = value;
_width = value;
}
}
public override double Width
{
get => _width;
set
{
_width = value;
_height = value;
}
}
}
Now, there are even those who are developing applications with your code. Everything seems perfect, your program works like a charm, but it is violating the Liskov substitution principle.
Suppose as part of your program growth, you decided to start writing unit tests, and wrote one like the following: see full code
Rectangle rectangle = new Square();
rectangle.Width = 3;
rectangle.Height = 6;
const double expected = 18;
var actual = rectangle.Area();
Assert.AreEqual(expected, actual);
There are some weird things ... however the code compiles and runs, however the test fails. And this is what the whole Liskov substitution principle is all about: the subtypes of a class should always behave like this. In other words: it derives from a class just to add capabilities, not to modify what it already has.
The solution is quite simple: we must make Square
not derived from Rectangle
, and instead implement IGeometrcShape
: see full code
public class Square : IGeometricShape
In this way, no one, not even yourself, can think that in this area, squares and rectangles are related.
Now, everything seems perfect, your program works wonders, but it is violating the principle of segregation of the interface.
Without having the slightest intention, we introduced this violation when we comply with the OCP, and that is that the ISP tells us that we must separate the interfaces so that the software components that work with them have only the information they need from them and no more. Let's take a look at the IGeometricShape
interface: see full code
public interface IGeometricShape
{
double Area();
double Perimeter();
}
And it is used both in the calculation of sum of areas and in the calculation of perimeters. But why should the class in charge of adding the perimeters have to know that the elements it works with also have an area?
Compliance with this principle leads us to separate the IGeometricShape
interface into two: IHasPerimeter
and IHasArea
, in order to pass only the necessary information to each of the methods within our program: see full code
public interface IHasArea
{
double Area();
}
public interface IHasPerimeter
{
double Perimeter();
}
public interface IGeometricShape : IHasArea, IHasPerimeter
{
}
Your code starts to grow, fantastic, so you think it's better to keep organizing it and move all the logic of the calculation of sums to another class: see full code
public class GreatCalculator
{
public double TotalAreas { get; private set; }
public double TotalPerimeters { get; private set; }
public void Calculate()
{
var figures = new IGeometricShape[]
{
new Rectangle {Width = 10, Height = 5},
new EquilateralTriangle {SideLength = 5},
new Rectangle {Width = 4, Height = 6},
new Square {SideLength = 10},
new Rectangle {Width = 8, Height = 9},
new Square {SideLength = 8},
new EquilateralTriangle {SideLength = 5}
};
var areaOperations = new AreaOperation();
var perimeterOperations = new PerimeterOperation();
TotalAreas = areaOperations.Sum(figures);
TotalPerimeters = perimeterOperations.Sum(figures);
}
}
And you made the necessary modifications to the main method of the program: see full code
private static void Main(string[] args)
{
var calculator = new GreatCalculator();
calculator.Calculate();
Console.WriteLine($"Total Area: {calculator.TotalAreas}");
Console.WriteLine($"Total Perimeter:{calculator.TotalPerimeters}");
Console.ReadKey();
}
Your code is ready to be expanded further. Everything seems perfect, your program works like a charm, but it is violating the dependency inversion principle.
This violation occurs in the new class you just added, right in the Calculate
method, and this is itself creating the figures it operates with (in the figures
array). What will happen in the future when you want to add another figure? Or when you want to change the size of some of the existing figures?
To comply with this principle we have to remove the dependency that the GreatCalculator
class has with the figures
arrangement, making the object that calls it the one in charge of providing it with the figures with which it has to operate: see full code
public void Calculate(IEnumerable<IGeometricShape> figures)
{
}
This reduces your dependency, and you are ready to trade any number of IGeometricShape
you want.
And ready, we have fulfilled the SOLID principles.