S[O]LID - Open/Closed Principle by example

Subscribe to receive new articles. No spam. Quality content.

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.

Subscribe to receive new articles. No spam. Quality content.

Comments