Note: I understand that this topic has been written and spoken about many times in past and most of what I would write in this post has been written somewhere prior. But I still think I ought to write a post on SOLID principles, as being a OOP developer it is almost my duty to know them and be able to apply them.
If I have missed some crucial points related to this topic, the readers are more than welcome to point that out and suggest appropriate corrections.
Last week I completed studying the pluralsight video lecture on SOLID principles of object oriented programming. It surprised me for all these years, I had considered myself an object oriented programmer, but was not well verse with SOLID principles.
I should admit that being able to code in a programming language like C# does not make one think object oriented, it is the SOLID principles that definitely give the correct direction.
The lecture was extremely insightful and explained every principle in great detail with associated code. In this blogpost I will be listing out the SOLID principles the way I understand and apply them.
1. Single Responsibility Principle:
Definition: There should never be more than one reason for a class to change.
My Notes: The way I think about this principle is whenever I have a need to write a new class, I should think of what responsibility the class should hold. In general the responsibility of a class can be looked at as the data it is suppose hold and the API (public methods) it should support. Now these two parts should represent a single responsibility.
For ex: If I am writing a class that reads data from datastore and also does some magic with retrieved data, it is definitely breaking SRP. In this case, it is better to create two classes, one that simply reads data from datastore and one that applies magic to the retrieved data. Another trick to recognize if you are breaking SRP is if your unit tests are hard to setup, then it is code smell that the underlying class is doing too much work and should be segregated in favor of Single Responsibility Principle.
2. Open-Closed Principle:
Definition: Software entities should be open for extension but closed for modification.
My Notes: The way to look at open-closed principle is once software entities i.e. classes, modules, functions etc. are implemented they should be modified only to correct errors. But if new features are to be added, one should create a new class instead of modifying original class.
Anytime, you find yourself modifying implementation of a software entity which was considered to be fully implemented once (possible deployed or shipped), you are likely breaking the open-closed principle. The way to accommodate new features is create a new class, and for reusability inherit from the original class.
3. Liskov Substitution Principle:
Definition: Subtypes must be substitute for their base types.
My Notes: At the heart of this principle is the concept of inheritance. You are likely to encounter this principle when you are about to design a class by inheriting a base class. If this subclass does not fit in all places where base class is used, you are likely breaking LSP.
A very interesting example that explains LSP is “why square is not a rectangle?“. Suppose, you have a Rectangle class with members height and width and public methods SetHeight() and SetWidth(). For a Rectangle it seems logical to be able to set both height and width separately. But if a Square class is derived from Rectangle, setting height would affect width and vice versa.
Personally, I have come across these kinds of argument many times with my colleagues where we are trying to fit a concept that seems logically inheritable in real world, but is not possible to design this way in terms of classes and objects. Now, I know why 🙂
4. Interface Segregation Principle:
Definition: Clients should not be forced to depend upon methods they don’t need.
My Notes: This principle is based on designing interfaces effectively. Whenever you are going to declare an interface, always try to design a thin interface, that is one containing only methods to extend a very specific contract.
For example, you want to write two classes, one that makes a web-service call to Facebook graph API and one that makes a web-service class to Instagram API in particular to get users photos, albums etc. If you create one fat interface containing both GetAlbums() and GetPhotos() methods, you are likely breaking ISP. Because, Instagram API does not and cannot support the get album functionality. But the Instagram api caller class that would implement this interface has to provide some default implementation for GetAlbums().
In this case ISP is being broken. Therefore in this limited context you should probably create two interfaces IPhotoGetter and IAlbumGetter. Both Facebook and Instagram classes would implement IPhotoGetter but only Facebook would implement IAlbumGetter.
5. Dependency Inversion Principle:
Definition: High level modules should not depend upon low level modules. Both should depend upon abstractions.
My Notes: This principle strongly supports design by abstraction and interfaces. You should think of this principle whenever you want to create a low level class that can be consumed by high level class.
For example, you have a low level class MySqlDbConnector that opens new connection to MySql database through public API ConnectToDB(…) and a high level class DataRetriever that directly calls into this low level class. This all works fine until there is a need to change database from MySql to lets say Sql server. In this case, you have to change the implementation of DataRetriever which is not good.
The way to effectively manage this is to create an interface IDbConnector that declares a contract ConnectToDb(…) and you have a MySqlDbConnector that implements this contract for MySql database connection, now if tomorrow, you have to replace MySql with Sql server, you needn’t change the implementation of DataRetriever, only change the instantiation from MySqlDbConnector to SqlServerDbConnector.
In order to fully understand and make use of this flexibility, one should use Dependency Injection techniques like Autofac. I will write about these dependency injection techniques in more detail in later blogpost.
The main purpose of this blogpost is to briefly note down the very important SOLID principles of OOD. One should always refer back to these principles when architecting there software components and also when refactoring code, adding new features, resolving bugs and so on.
Having good understanding of SOLID principles should reduce your efforts in fabricating a seemingly logical real-world situation in terms of software entities and provide a methodical approach your architecting efforts in general.