Open-Closed Principle
Plan
General Principle
Practical example
Why it matters
Applying it in our codebase
What is it?
SOLID Principles
OOP Guidelines
Single-Responsibility
Open–Closed
Liskov Substitution
Interface Segregation
Dependency Inversion
Open-Closed
Credited to Bertrand Meyer 🇫🇷
Popularized by Bob Martin (SOLID)
🤓
General principle
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
🤔 🤨 🧐
Open for extension,
but closed for modification
=
We should 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.
Extend the behaviour without changing it.
🤔 🤨 🧐
How??
Using the right abstraction
Example 1
# NOT OC
class Purchase
def charge_user!
charge_with_paypal
send_confirmation
end
def charge_with_paypal
verify_paypal_account
Paypal.charge(user: user, amount: amount)
end
end
# OC
class Purchase
def charge_user!
payment_processor.charge(user: user, amount: amount)
send_confirmation
end
end
class PaypalProcessor
def charge(user:, amount:)
# charge user /w Paypal logic
end
end
class StripeProcessor
def charge(user:, amount:)
# charge user /w Stripe logic
end
end
Example 2 - Strategy pattern
# ------ NOT OC -----
class NotificationSender
def send(user, message)
EmailSender.send(user, message) if user.active?
end
end
# ----- OC -----
class NotificationSender
def send(user, message, sender = EmailSender.new)
sender.send(user, message) if user.active?
end
end
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
sender = NotificationSender.new
sender.send(user, "Wesh gros", SmsSender.new)
Example 3
# ------ NOT OC -----
module CoffeeShop
class Purchase
attr_reader :user
def initialize(user)
@user = user
end
def perform
user.give(hot_coffee)
user.charge(amount)
end
private
def hot_coffee
cup.fill(coffee)
cup
end
def cup
@_cup ||= PaperCup.new
end
def coffee
expresso = ExpressoMachine.make_expresso
expresso.add_sugar(pieces: 1)
expresso
end
def price
4
end
end
end
Why it matters
Know why you do it first
- Trusting established patterns
- We don't know how our code is going to change in the future. Write adaptable code to the future.
- Easier to test
- You never break your tests when adding new features
- You never cause some distant side-effect
- Help to respect SRP
When to use it
- No class can be open to all extensions. You have to choose which extensions make sense.
Guess which parts are the most likely to be extended. - "When I open a class for a particular reason, I will probably reopen it for the same reason in the future."
Our codebase
Recommended resources
- Thoughtbot - Article: Back to Basics: SOLID
https://thoughtbot.com/blog/back-to-basics-solid - Thoughtbot - Upcase - Video: Open-Closed Principle
https://thoughtbot.com/upcase/videos/open-closed-principle - Sandi Metz - Article
https://sandimetz.com/blog/2014/05/28/betting-on-wrong - Sandi Metz - Talk: All the Little Things
Open/Closed Principle
By durev
Open/Closed Principle
- 129