S[O]LID - Open/Closed Principle by example
- 28 May 2017
- Patterns Ruby Design
- 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 EmailSender
class.
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
Because 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 user
and message
.
We can extend functionality without changing implementation of NotificationSender
.
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.
Read more about SOLID Principles in case if you missed it: