SOLID Design Principles

SOLID Design Principles are the most important principles in object-oriented design. They were first introduced by Robert C. Martin, popularly known as Uncle Bob. It is an acronym for the first five object-oriented design(OOD) principles which, when combined together, make it easy for a programmer to develop software that is easy to maintain and extend. They also make it easy for developers to avoid code smells, easy refactor code and are also a part of the agile or adaptive software development. If we look at the design patterns categorization hierarchy, SOLID Design Principles is at the top of the hierarchy.

Behind the meaning of the acronym SOLID

Let’s now, try to explain what is behind the acronym SOLID. Under each letter of the acronym is one individual design principle.

Let’s look at each principle to understand how SOLID can help make us better developers.

Single-responsibility Principle

Single-responsibility Principle or short SRP gives us a very good advice on how to build a class. It states that A class should have one and only one reason to change, meaning that a class should have only one job.

Let’s image that we need to create a Journal object that will enable the user to add, remove and save the entries. The Journal class definition could look like this:

public class Journal
{
public int AddEntry(string text)
{
//Some logic for adding entries
}

public void RemoveEntry(int index)
{
//Some logic for removing entries
}
}
// handles the responsibility of persisting objects
public class Persistence
{
public void SaveToFile(Journal journal, string filename, bool overwrite = false)
{
//Some logic for storing to persistance
}
public Journal LoadFromFile(string filename)
{
//Some logic for loading frompersistance
}
}

The definition looks pretty good but this implementation violates single responsibility principle. The reason why we are violating this is because we are adding to much responsibility to the Journal class. As you can see, the Journal is responsible not only for adding and removing the entries but also for managing persistence, so you will have more than one reason to change the Journal. We will need to change the class implementation so that the class will only have one responsibility and that is working with entries. So, what we need to do is to remove the methods Save and Load from Journal into a separate class that will have the responsibility for working with persistence storage.

public class Journal
{
public int AddEntry(string text)
{
//Some logic for adding entries
}
public void RemoveEntry(int index)
{
//Some logic for removing entries
}
}
// handles the responsibility of persisting objects
public class Persistence
{
public void SaveToFile(Journal journal, string filename, bool overwrite = false)
{
//Some logic for storing to persistance
}
public Journal LoadFromFile(string filename)
{
//Some logic for loading frompersistance
}
}

Open-closed principle

The open-closed principle says that Objects or entities should be open for extension, but closed for modification. This means that a class should be easily extendible without the need to modify the class itself, so you can add a new functionality to a class without modifying the body of the class. This sounds like it’s easily said than done. Let’s presume that we have some kind of filter object for our Journal:


public class JournalFilter
{
public IEnumerable<Journal> FilterByEntryDate(IEnumerable<Journal> journals, DateTime date)
{
//Some implementation
}
public IEnumerable<Journal> FilterByAuthor(IEnumerable<Journal> journals, string author)
{
//Some implementation
}
public IEnumerable<Journal> FilterByEntryDateAndAuthor(IEnumerable<Journal> journals, DateTime date, string author)
{
//Some implementation
}
}

As you can see, if we need to add another filter we need to go into JurnalFilter class and add a new method to extend the functionality of a class and this clearly breaks the open-close principle. So how should we modify the class so that we satisfy the open-close principle? We will introduce a bunch of interfaces and also use Specification pattern. Let’s create our interfaces.// we introduce two new interfaces that are open for extension


public interface ISpecification<T>
{
bool IsSatisfied(Journal p);
}
public interface IFilter<T>
{
IEnumerable<T> Filter(IEnumerable<T> items, ISpecification<T> spec);
}

Specification pattern ISpecification interface has only one method and that is: “if some criteria is satisfied”, for example this will be the pattern for filtering our Journal’s. IFilter interface will be our implementation of filter that will replace impemented methods in our existing JournalFilter class. So how should we replace existing FilterByEntryDate method? Let’s first replace the predicate for filtering EntryDate. We will create the class that implements ISpecification interface. This will be our criteria for filtering by EntyDate.


public class DateSpecification : ISpecification<Journal>
{
private DateTime date;
public DateSpecification(DateTime date)
{
this.date = date;
}
public bool IsSatisfied(Journal p)
{
return p.EntryDate == date;
}
}

As you can see, if you need another criteria for the filter, you will need to create a new class that will implement the predicate logic and implements ISpecification interface. So, in this way, the new filter class will be open for extension but closed for modification. Let’s see how to create a new filter class by implementing an IFilter interface.


public class JurnalFilter : IFilter<Jurnal>
{
public IEnumerable<Jurnal> Filter(IEnumerable<Jurnal> items, ISpecification<Jurnal> spec)
{
foreach (var i in items)
if (spec.IsSatisfied(i))
yield return i;
}
}

But what about FilterByEntryDateAndAuthor method? We have two criteria and an operator AND? Well, in this case, we will create a combinatory specification class that will inherit ISpecification interface.


public class AndSpecification<T> : ISpecification<T>
{
private ISpecification<T> first, second;
public AndSpecification(ISpecification<T> first, ISpecification<T> second)
{
this.first = first ?? throw new ArgumentNullException(paramName: nameof(first));
this.second = second ?? throw new ArgumentNullException(paramName: nameof(second));
}
public bool IsSatisfied(Product p)
{
return first.IsSatisfied(p) && second.IsSatisfied(p);
}
}

As you can see, we didn’t change the existing JurnalFilter class and we can use the new filter on the JurnalFilter object.


var filter = new JurnalFilter ();
var dateResults =  filter.Filter(jurnals, new DateSpecification(date));
var authorResults =  filter.Filter(jurnals, new AuthorSpecification(author)));
var combination = filter.Filter(jurnals,
new AndSpecification<Jurnal>(new DateSpecification(date), new AuthorSpecification(author)));

Liskov substitution principle

The idea here is simple. You should be able to substitute a base type for a sub-type. Basically, what this means is that the objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. Let’s observe the following scenario, we have a rectangle object in which we want to define width and height.


public class Rectangle
{
public int Width { get; set; }
public int Height { get; set; }
public Rectangle()
{
}
public Rectangle(int width, int height)
{
Width = width;
Height = height;
}
}

Let’s presume that we want to create a new class that will inherit a Rectangle class and we want to create a separate implementation for Width and Height.


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

And let’s say that we have a method that calculates the area of the object.


public int Area(Rectangle r)
{
return r.Width * r.Height;
}

Now, if we execute the following code we will get result 16 for both cases and this is perfectly valid case:


Rectangle rc = new Rectangle(2,3);
Area(rc)
Square sq = new Square();
sq.Width = 4;
Area(sq)

But since we are using inheritance it is perfectly legal to store an instance of a Square object into Rectangle. And if you change the code you should get the same result.


Rectangle rc = new Rectangle(2,3);
Area(rc)
Rectangle sq = new Square();
sq.Width = 4;
Area(sq)

But if wrong, you will receive a zero for Square area calculation. In the existing implementation of Rectangle and Square classes, Liskov substitution principle is violated. This is because we are using a new keyword for properties in our inherited class and instead of this we should override the base values. So, to adapt to the principle we should change the Rectangle class properties into virtual.


public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public Rectangle()
{
}
public Rectangle(int width, int height)
{
Width = width;
Height = height;
}
}

And instead of a new keyword, you should override the inherited properties.


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

And now your implementation is compliant with Liskov substitution principle.

Interface segregation principle

Interface segregation principle is something that you likely have or will encounter in a real world where you are building interfaces that are just too large. The idea is that the interfaces should be segregated so that nobody who implements the interface has to implement the functionalities they don’t need. Clients should never be forced to implement an interface or depend on methods they do not use. Let’s create an interface for printer operation. Today’s printers can print, scan, send e-mail, etc. so let’s create such interface.


public interface IPrinter
{
void Print(Document d);
void SendMail(Document d);
void Scan(Document d);
}

Now let’s implement the printer class that will adapt to the interface


public class Printer : IPrinter
{
public void Print(Document d)
{
//Implementation
}
public void SendMail(Document d)
{
throw new System.NotImplementedException();
}
public void Scan(Document d)
{
throw new System.NotImplementedException();
}
}

Let’s say that we don’t need SendMail and Scan operation, but since we are implementing an interface we need to implement all methods of the interface, so we will add NotImplementedException. When you implement this kind of exception, then you are violating Interface segregation principle. You will need to split your big interface into lots of smaller interfaces which are more atomic and have only one concern. When you create an interface remember this principle: You Ain’t Going to Need It or YAGNI. For example, we should split the IPrinter interface into three interfaces for each operation.


public interface IPrinter
{
void Print(Document d);
}
public interface IScanner
{
void Scan(Document d);
}
public interface IEmail
{
void SendMail(Document d);
}

Dependency Inversion principle

If you have worked with ASP.NET Core you have naturally used this principle because the framework adapts to this principle in its core. Also, if you have used Dependency Injection with some kind of dependency container like Ninject in the older version of ASP.NET core, you have also been adapting to this principle. The principle says that Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but it should depend on abstractions. For this principle let’s take for an example our existing example from Open-closed principle. We have our interfaces and implementation of the interface.


public interface ISpecification<T>
{
bool IsSatisfied(Journal p);
}
public interface IFilter<T>
{
IEnumerable<T> Filter(IEnumerable<T> items, ISpecification<T> spec);
}
public class JurnalFilter : IFilter<Jurnal>
{
public IEnumerable<Jurnal> Filter(IEnumerable<Jurnal> items, ISpecification<Jurnal> spec)
{
foreach (var i in items)
if (spec.IsSatisfied(i))
yield return i;
}
}

Now let’s used the implementation in our Journal class


public class Jurnal
{
priate JurnalFilter _filter;
public Jurnal(JurnalFilter  filter)
{
_filter = filter;
}
}

What if we now want to implement a DocumentFilter that will adapt to IFilter interface into Journal class? We can not do this without the need to modify the Journal class itself, so we are clearly violating Open-closed principle along with the Dependency Inversion Principle. So to fix this issue we need to use dependency injection and replace real implementation in the constructor with abstraction.


public class Jurnal
{
private IFilter<Jurnal> _filter;
public Jurnal(IFilter<Jurnal>  filter)
{
_filter = filter;
}
}

Conclusion

Now that we have looked at each principle in detail let’s summarize everything. By using these principles, you will become a better developer and you will create more maintainable and extensible software. Also, your software will be more testable because it will be easier to create a unit test for code that adapts to SOLID. In addition, these principles are the entry knowledge that you have to know before you go deeper into design patterns. Finally, let’s take a look again and memorize our graphical representation of SOLID Design Principles.