SOLID: Open-Closed Principle in Ruby


Created by Stan on 18-04-2023


The Open/Closed Principle (OCP) is one of the five principles of object-oriented programming and design known as SOLID. The OCP states that a class should be open for extension but closed for modification. In other words, you should be able to add new functionality to a class without modifying its existing code. This principle promotes a more robust and maintainable software design by reducing the likelihood of introducing bugs when adding new features. In this article, we will explore the Open/Closed Principle and demonstrate its application in Ruby.

Understanding the open closed principle

The OCP revolves around two primary concepts:
- Open for extension: You should be able to add new features or behaviors to a class without affecting its existing functionality.
- Closed for modification: The source code of a class should not need to be modified when adding new functionality.

By adhering to the OCP, you can create a more flexible and maintainable codebase that is less prone to bugs and more adaptable to changing requirements.

Applying the Open/Closed Principle in Ruby

Let's illustrate the Open/Closed Principle using a Ruby example. Imagine we have a system that generates reports in different formats, such as HTML and JSON.

We'll use the JSON and Nokogiri gems to create JSON and HTML reports, respectively. First, you'll need to install the gems:

gem install json nokogiri

Here's an initial implementation without considering the OCP:

require 'json'
require 'nokogiri'

class ReportGenerator
  def initialize(data)
    @data = data
  end

  def generate(format)
    case format
    when :html
      generate_html
    when :json
      generate_json
    end
  end

  private

  def generate_html
    builder = Nokogiri::HTML::Builder.new do |doc|
      doc.html do
        doc.head { doc.title 'Report' }
        doc.body do
          doc.h1 'Report Data'
          doc.table do
            doc.tr do
              data.keys.each { |key| doc.th key }
            end
            doc.tr do
              data.values.each { |value| doc.td value }
            end
          end
        end
      end
    end
    builder.to_html
  end

  def generate_json
    data.to_json
  end
end

The problem with this implementation is that whenever we need to support a new report format, we must modify the ReportGenerator class by adding a new method and updating the generate method. This violates the Open/Closed Principle.

To refactor this code and adhere to the OCP, we can use polymorphism and separate the report generation logic into different classes:

class ReportGenerator
  def initialize(data)
    @data = data
  end

  def self.generate(data)
    new(data).generate
  end

  def generate
    raise NotImplementedError, "'generate' method should be implemented"
  end
end
require 'nokogiri'

class HTMLReportGenerator < ReportGenerator
  def generate
    builder = Nokogiri::HTML::Builder.new do |doc|
      doc.html do
        doc.head { doc.title 'Report' }
        doc.body do
          doc.h1 'Report Data'
          doc.table do
            doc.tr do
              @data.keys.each { |key| doc.th key }
            end
            doc.tr do
              @data.values.each { |value| doc.td value }
            end
          end
        end
      end
    end
    builder.to_html
  end
end
require 'json'

class JSONReportGenerator < ReportGenerator
  def generate
    @data.to_json
  end
end

Now, when we need to support a new format, we can simply create a new class that implements the generate method without modifying the existing code:

class XMLReportGenerator < ReportGenerator
  def generate
    # add your own code to generate a report in XML format
  end
end

In this example, the HTMLReportGenerator class uses the Nokogiri gem to build an HTML report with a table containing the data. The JSONReportGenerator class uses the JSON gem to convert the data to a JSON-formatted string.

Now you can use the ReportGenerator class to generate reports in different formats:

data = { title: 'Sample Report', views: 100, clicks: 20 }

html_report = HTMLReportGenerator.generate(data)
puts html_report

json_report = JSONReportGenerator.generate(data)
puts json_report

xml_report = XMLReportGenerator.generate(data)
puts xml_report

This refactored code adheres to the Open/Closed Principle by allowing new report formats to be added without modifying the ReportGenerator class.

The Open/Closed Principle is an essential concept in object-oriented programming and design that promotes robust, maintainable, and flexible code. By applying the OCP in your Ruby projects, you can create a more adaptable and resilient codebase that is better prepared to handle changing requirements and less prone to bugs.

Note:

The code in the sample has been updated where we no longer inject the subclasses but call the generate method on the subclasses directly.



Related Posts