SOLID Principle for Rails
Michael Feathers and Bob Martin introduced the definition of SOLID principles for object-oriented design.
The term “solid principle” refers to a collection of five fundamental principles that help developers to create flexible solutions, identify software bugs, and refactor their code to avoid problems. And in order to write efficient, adaptable, and clear code, this approach must be followed.
Importance of solid principles
it’s utilised to prevent bad software designs. It’s a coding standard that enhances code’s extensibility, logic, and readability. Design the software in such a manner that the code is reusable and flexible.
Additionally, helps to dividing the code which makes modifications less painful
Why do we need it to maintain that in our application?
A minor modifications can appear as a bug due to poor software designs
To maintain code’s readability, re-usability (means capable of being used again or repeatedly), logic, and extensibility (means meant to allow the inclusion of additional features) for that we applied the SOLID principle.
Suppose we have new functionality to implement so at that time we modify into current code which is tested and deployed over production. There might be a change that break our functionality of entire class which need to refactoring latter. So, we need to structure it in such a way that not impact on your current code and functionality.
These five principles are part of the SOLID design principle:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Let’s explore each principle in detail one by one.
Single Responsibility Principle
A class should have just one, reason to change.
Each class and module focus on a single task at a time and should be related to single purpose. In this way class become smaller and cleaner.
Here we take an example of an invoice billing system. Consider you are purchasing something and then you get notification of an email which contains a report as a PDF or any other format.
Here, we have a class called InvoiceMailer, and within it, we carry out two distinct functions, the first is to generate reports and the second is send email to users. As mailer is the name of the class, it shouldn’t perform tasks that are not meant for it.
According to the single responsibility concept, each class, module, method, etc. should only be responsible for one aspect of a program’s functionality. In order to separate them out.
In fact, if we need to alter the feature for generating and sending emails, those changes will not have an impact on one another. Code breaking chances are thus extremely rare.
Open/Closed Principle
Software entities such as classes, modules, methods, etc should be open for extension but close for modification
This principal’s primary goal is to design the code’s architecture in such a way that functionality can be extended rather than modified or refactored.
Let’s work with an example which is mentioned in SRP in which we now have functionality that enables us to deliver reports in forms other than CSV ,PDF, etc.
This modifies already exist classes and methods. And keep in mind that if additional formats become available in the future, we will add code to the generator method.
Let’s now reflect the mentioned features using OCP.
Therefore, in the future, rather than modifying the functionality, we can simply add a class and send the class name as a parameter.
Liskov Substitution Principle
if S could be a sub-type of T, then objects of type T is also replaced with objects of type S
Sub-classes should add to a base class’s behaviour, not replace it.
A parent instance should be able to be replaced by one of its children without causing any unexpected or bad behaviour. LSP helps developers create more reusable code and better structure class hierarchies by ensuring that abstractions are valid.
Consider that we need to generate two different types of reports, one is when an order is successfully created then send an invoice report to the user. And second one is that after a user successfully subscribes to a plan, a report is generated for the user.
Currently, this principle is only applied to inheritance. Therefore, we must use ReportGenerator as our base class.
While this example is perfect for an InvoiceReport, it fails for a UserReport. This is an example of a Liskov violation.
The behaviour of both classes needs to be identical so that the user cannot differentiate between them. By behaviour, we mean that the class’s methods should be consistent. The methods in these classes should be characterised by methods that have a similar name, the same number of arguments, the same kinds of data, and the same return type.
Next, we define a set of methods that are required for any subclass that inherits the ReportGenerator class. In the base class, we define the after_report_create method.
This principle helps to easily replace any subclass without breaking things and without having to make a lot of changes.
Here, we have defined the after_report_create method, which contains a raise statement. So, any that inherits the base class needs to have the after_report_create method.
If it is not present, this may raise an error that the method is not implemented. In this way, we will confirm that the subclass is consistent. With this, the user can always make sure that the after_report_create method is present.
Interface Segregation Principle
No client should be forced to depend on methods that they don’t use.
In order for clients to know about the interfaces that are relevant to them, one large interface needs to be divided into several smaller and relevant interfaces.
Since Ruby and other dynamically typed languages (duck typing languages) depend on the methods and properties of objects rather than just their type to determine compatibility, this principle doesn’t really apply to them. Sorry, no code example available.
Dependency Inversion Principle
High level modules should not depend upon low level modules. Both dependent upon abstraction
Abstraction shouldn’t rely upon details, Details should depends upon abstraction
DIP is the outcome of two SOLID principles: Liskov Substitution and Open-Closed Principle
It must also be readable, customizable, and simple to replace with other instances of a base class without the system crashing.
Take an invoice report generator as an example. We have generated invoice reports in a number of file types, including PDF and CSV.
Instead of abstractions, the InvoiceReport in this case depends on the ReportGenerator class
The logic that references other classes may be found in the ReportGenerator class’s methods parse_pdf and parse_csv. As a result, when we change the class InvoiceReport, it may affect all the connected classes as well. It demonstrates the DIP principal’s failure
Thus, if we change the ReportGenerator class’s methods parse_pdf and parse_csv logic which are present, it may have an effect on all the linked classes.
Let’s now find a workaround and separate the code that depends on one another. As we create invoice reports, let’s create classes for various format types and pass class names as arguments.
ParseCSV and ParsePDF, as well as any other format, are easily adaptable.
Moreover, we can ensure the consistency of the ParserConverter subclass. By doing this, the user may be sure that the parse method is always available.
Here, InvoiceReport is independent of ParseCSV and ParsePDF and is able to work with any format that provides a parse method.
Additionally, it is simple and less difficult to switch between report generators for any format.
I believe I have covered all of this. Adopting the SOLID principle in your software is also a good idea to prevent errors. We appreciate you taking the time to read this document 🙌.