Foundation - Open Closed Principle

Foundation - Open Closed Principle

The acronym SOLID is quite popular in the OOP realm, but I tend to notice that the “O” which stands for Open Closed Principle tends to get overlooked.

What is this principle?

The Open-Closed Principle (OCP) states that software entities (classes, modules, methods, etc.) should be open for extension, but closed for modification.

Enough about theory let’s see an example.

class ReportService
  att_reader :user

  def initialize(user)
    @user = user
  end

  def generate
    if user.with_premium_subscription?
      send_report(extensive_report)
    else
      send_report(basic_report)
    end
  end

  private

  def basic_report
    # generate a limited report version for the last 30 days
  end

  def extensive_report
    # generate an extensive report version for the last 90 days
  end

  def send_report(data)
    # send content to `user.email_address`
  end
end

I’m generating and sending a report based on the user subscription model in this service class. The new requirement introduced a new type of membership, let’s name it “VIP”. The acceptance criteria are:

For users with a “VIP” membership, add a suggestion section to the report and send an SMS notification with the email.

The typical way of handling this new change would be to go and edit the generate method by adding a new if condition. That’s how the OCP principle isn’t respected as it should be closed for modification but open to extensions. To achieve that we can use the strategy pattern.

class UserReport
  attr_reader :user

  def initialize(user)
    @user = user
  end
  def send_report
    raise NotImplementedError
  end

  def prepare_data
    raise NotImplementedError
  end

  private

  def send_email_to(email_address, content)
    # format content for the email template
    # send the email to the specified address
  end

end

class BasicReport < UserReport

  def send_report
    send_email_to(user.email_address, prepare_data)
  end

  def prepare_data
    # generate a limited report version for the last 30 days
  end
end

class PremiumReport < UserReport

  def send_report
    send_email_to(user.email_address, prepare_data)
  end

  def prepare_data
    # generate an extensive report version for the last 90 days
  end
end

class VIPReport < UserReport

  def send_report
    send_email_to(user.email_address, prepare_data)
    send_sms_notification_to(user.mobile_number)
  end

  def prepare_data
    # generate an extensive report version for the last 90 days with a suggestion section
  end

  private
  def send_sms_notification_to(mobile_nbr)
    # send a notification sms with a customizable text content
  end
end

class ReportService
  attr_reader :user_report

  def initialize(user_report)
    @user_report = user_report
  end

  def generate
    user_report.send_report
  end
end


## Usage example

report_service = ReportService.new(VIPReport.new(vip_user))
report_service.generate

This is a simplified example but in real-life cases, you will notice a lot of if or case conditions based on a specific field or type. These conditions tend to affect most methods until they start spiraling out of control.

That’s it, so have fun and keep coding 🙂