2 min read

Open Closed Principle

The Open/Closed Principle, a principle of object-oriented programming, emphasises that classes or methods should be designed in a way that allows for extension (adding new functionality) without modification (changing existing code). This principle promotes the idea of designing code that is flexible, maintainable, and resilient to change.

The rationale behind the Open/Closed Principle is that modifying existing code can introduce risks of introducing bugs or un-intensionally altering the behaviour of a system. Therefore, it is preferable to avoid modifying tested and reliable code whenever possible. Instead, the focus should be on extending the code to accommodate new functionality, without altering the existing code.

To achieve this, modular designs that promote separation of concerns and encapsulation are often used. One common design pattern that aligns with the Open/Closed Principle is the Strategy Pattern. The Strategy Pattern involves encapsulating algorithms or behaviours as separate classes, and allowing them to be swapped out at runtime. This way, new behaviours can be added or existing behaviours can be changed without modifying the core code of the system.

By following the Open/Closed Principle and designing code that is open for extension but closed for modification, developers can create code that is more maintainable, adaptable, and less prone to introducing unintended bugs. It promotes good coding practices and helps create software systems that are easier to evolve and extend over time.

In Ruby, classes are open, meaning that you can modify or extend them even after they are defined. However, it's important to be cautious when monkey-patching (i.e., modifying) a class directly, as it can have unintended consequences and affect all the dependencies that call that class.

To adhere to the Open/Closed Principle in Ruby, it's generally recommended to prefer subclassing or using composition over directly modifying existing classes. This way, you can create a new class or module that inherits from or collaborates with the original class, respectively, to implement changes or extensions, without modifying the original class itself.

Example:

class Order
  def initialize
    @items = []
    @total_cost = 0
  end

  def add_item(item, price)
    @items << [item, price]
    @total_cost += price
  end

  def apply_discount(discount)
    @total_cost -= discount
  end

  def total_cost
    @total_cost
  end

  # Other methods for calculating total cost, generating order reports, etc.
end

class DiscountStrategy
  def calculate_discount(total_cost)
    raise NotImplementedError, "Subclasses must implement this method"
  end
end

class TenPercentOffDiscount < DiscountStrategy
  def calculate_discount(total_cost)
    total_cost * 0.1
  end
end

class FixedAmountDiscount < DiscountStrategy
  def initialize(amount)
    @amount = amount
  end

  def calculate_discount(total_cost)
    total_cost - @amount
  end
end

# Usage:
order = Order.new
order.add_item("Item 1", 100)
order.add_item("Item 2", 200)
puts "Total cost without discount: $#{order.total_cost}"

ten_percent_discount = TenPercentOffDiscount.new
order.apply_discount(ten_percent_discount.calculate_discount(order.total_cost))
puts "Total cost with 10% off discount: $#{order.total_cost}"

fixed_amount_discount = FixedAmountDiscount.new(50)
order.apply_discount(fixed_amount_discount.calculate_discount(order.total_cost))
puts "Total cost with $50 fixed amount discount: $#{order.total_cost}"

In this example, the Order class has a DiscountStrategy base class that defines a common interface for calculating discounts. The TenPercentOffDiscount and FixedAmountDiscount classes are implemented as subclasses of DiscountStrategy and provide specific discount calculation logic.

The Order class has a apply_discount method that takes a discount value as an argument, which is calculated by calling the calculate_discount method on the appropriate discount strategy object.

This allows for different discount strategies to be applied dynamically at runtime without modifying the Order class. New discount strategies can be easily added by implementing a new subclass of DiscountStrategy and providing the discount calculation logic in the calculate_discount method, without modifying the Order class.