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

Open/Closed Principle

By durev

Open/Closed Principle

  • 129