S[O]LID - Open/Closed Principle by example
In my previous article I covered Single Responsibility Principle. Today I'm going to write about the "O" in SOLID - Open/Closed Principle.
Definition of Open/Closed Principle says:
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
When I read this definition first time I got stuck because it was so hard to figure out how something can be open to extension but closed for modification. How can we extend functionality without changing code, right?
But we can achieve that. Let me show you by example how we can do that.
class NotificationSender def send(user, message) EmailSender.send(user, message) if user.active? end end
Pretty simple code. We want to send notification to user, but only if user is active (we could have much more complex logic to define if we want to send notification at all there).
This implementation is not open for extension. To prove that, let's say that we want to switch from email notifications to text messages. There is no way to do that because we have hard-coded dependency on
To improve our code we can use Dependency Injection for that.
Let's inject sender into that method:
class NotificationSender def send(user, message, sender = EmailSender.new) sender.send(user, message) if user.active? end end
Look at that one simple change we made. All code in our app will work, because
sender is an optional argument. And since we provided default value - it will use
EmailSender as a default sender. Small change but it brings a lot of flexibility.
We can see that
NotificationSender#send is going to call
send method on
sender. That allows us to implement any sort of senders we want.
On Wikipedia page they say about two types of Open/Closed Principle:
- Meyer's open/closed principle
- Polymorphic open/closed principle
Both of those rely on inheritance. We can use inheritance as well to make sure that each sender supports
send method. Possible implementation could look like this:
class Sender def send(user:, message:) raise NotImplementedError end end class EmailSender < Sender def send(user:, message:) # implementation for Email end end class SmsSender < Sender def send(user:, message:) # implementation for SMS end end
NotificationSender is accepting any sender, now we can easily extend functionality by new type of sender.
sender = NotificationSender.new sender.send(user, "Hello World", SmsSender.new)
In this case if user is active he will receive SMS.
If we want to add other sort of notification, all we need to do is to create class, inherit it from
Sender and implement
send method which would accept
We can extend functionality without changing implementation of
One of articles about Open/Closed principle described it this way:
What we are basically talking about here is to design our modules, classes and functions in a way that when a new functionality is needed, we should not modify our existing code but rather write new code that will be used by existing code.
That's exactly what we have now. We can add new code that would implement new functionality without a need to modify existing code.
Thanks for reading and feedback you give me.