Make It Real Elite — Week 8: S.O.L.I.D
Last time we’ve talked about databases and their main features, I hope you found that useful. Today it’s about to get very solid, so stay close. :)
I would like to bring on the table the concept of S.O.L.I.D. developed by Robert C. Martin, known also as Uncle Bob. Well, Uncle Bob, published 5 design principles with the goal to make your code more flexible, bless him! Here is what S.O.L.I.D principles stand for:
- Single Responsability Principle
- Open-closed Principle
- Liskov Sustitution Principle
- Interface segregation principle
- Dependency Inversion Principle
In this article we will go through each one of them, we’ll write some code using Ruby and we will comment the most important aspects for each principle. Let’s start!
Single Responsability Principle
A class should have one and only one reason to change, meaning that a class should have only one job.
Thing is that it violates the SRP principle since this class is in charge to create and notify the new invoices.
Let’s go through a small imagination game to ease the explanation. We want to choose a different email provider or to custom the email notification before notifying our customers about their invoices. All this logic will be into the
Invoice class. Thus, in order to enforce the SRP principle we can handle both functions in separated classes we preserve the
Invoice class, and we can also create the
InvoiceMailer class which will be in charge to send the email.
Any other logic, as for instance, the mailer provider, custom attributes or whatever is related to any change for the mailing notification should be implemented into this new class and not into the
Invoice class as we had in the first example.
Objects or entities should be open for extension, but closed for modification.
Due to the straightforward description of this principle is very clear, we’ll move directly into the code.
InvoiceNotifier class implements the
notify method and depending on the channel type it receives, it will use a channel to send the invoice notification to the customer. However, imagine we have 20 different ways to notify our customers so our
notify method will have 20 different options in the case statement and every new channel type will need a modification on this class.
To enforce this principle we reimplement the
notify method. Now it expects to receive a notifier argument, but the question here is: what is a notifier? The notifier argument will represent an object with the
send method implemented into its definition — in Duck Typing we’ll trust for this as one of the keys points of Ruby as a programming language.
Each channel type, as you can see from the code above, will have a dedicated class. Within this class we will define the logic necessary to deliver each notification. The default channel will be the
InvoiceMailer.new, so if the method does not receive any argument, an email will be sent out. A new channel type won’t need any change if we keep the consistency and we guarantee the implementation of the
send method for each of them.
Liskov Sustitution 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.
The definition for this principle isn’t easy peasy lemon squeezy, so let’s delve into alternative definitions to fully get this one:
Subtypes must be substitutable for their base types
So now we have a better place to start a
Corporation have to
make_money. The only difference between both of them is that
Corporations are able to run investment rounds, or what we called
stock_sale in our implementation. However, we can describe a
Corporation as a child of a company.
We’ll have to pay attention as here we are violating the LSP since a
Company does not implement all the methods that a
Corporation has. Simply put, a
Corporation cannot be substitutable for a
Company. In Ruby fix this issue is really simple as we only have to keep in mind that interfaces for parents and children must be the same, thus the methods from the child should be implemented into its parent too.
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.
ISP is a common mistake we make when we share the same interface between different models. There is very likely that all of the methods from our interface aren’t used by each of the clients.
In order to keep consistency, this principle proposes to separate and to import interfaces with the guarantee that all the methods within each of them will be used or consumed by each client. In our example, the
Controller class uses partially the
Airplane. Check upon it and upon the changes we made to fix the issue. Let me know if you have any comment or question within the responses section.
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.
Finally we are at the last principle, If you look the
InvoiceNotifier class it depends on the implementation for each notifier channel, although we have and abstraction for a channel into its own class, we are hardcoding some classes into this class. To fix this problem we can use the same strategy as we did it for the OCP, we wrap this logic into a new abstraction and any dependency injection into the
InvoiceNotifier class is removed.
And so we are done for today. I hope this post could be really helpful to understand each of the S.O.L.I.D principles. If you can enforce your code with solid OOP foundations, it will be a good asset to allow flexibility for the future.
Stay tuned for what’s coming next, see you!