SOLID OOP Development: Open Closed Principle Guide and Example
Continuing on our discussion of SOLID development principles, in this guide I’m going to walk through the: open closed principle.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

Continuing on our discussion of SOLID development principles, in this guide I’m going to walk through the: open closed principle.

large

For some reason or another developers seem to struggle understanding this SOLID element more than some of the others. With this in mind I’m going to give a dead simple explanation of how the open closed principle works, followed by a Ruby code example.

I think that once you see how powerful this concept is you’ll fall in love with it. Because at it’s core it allows you to scale your code without having to worry about wasting time on legacy classes.

Open Closed Principle Definition

A dead simple explanation of the open closed principle is this:

Software elements (classes, modules, functions, etc.) should be open for extension, but closed for modification

Essentially this means that you should build your classes in a way that you can extend them via child classes. AND that once you’ve created the parent class it no longer needs to be changed.

The original concept was credited to Bertrand Meyer when he coined the term back in 1988 in his book Object Oriented Software Construction.

Surprisingly Challenging Task

If you’ve never attempted this design pattern the concept may seem straightforward. However I think you’ll find this is a skill that takes practice and repetition (much like any other advanced development task).

Open Closed Principle Example

To understand how the open closed principle works let’s look at a practical example.

The Naive Approach

class OrderReport
  def initialize(customer:, total:)
    @customer = customer
    @total = total
  end

  def invoice
    puts "Invoice"
    puts @customer
    puts @total
  end

  def bill_of_lading
    puts "BOL"
    puts @customer
    puts "Shipping Label..."
  end
end

order = OrderReport.new(customer: "Google", total: 100)
order.invoice
order.bill_of_lading

I’ve created an OrderReport class that contains some attributes such as customer and total. In addition to the attributes the class also contains a couple of methods. Such as:

  • An invoice method that prints out the details associated with an order.
  • Along with a bill_of_lading method that prints out the order report for shipping purposes.

If we run the program you’ll see that it works perfectly fine and prints out the values for each method.

Invoice
Google
100
BOL
Google
Shipping Label...

I Don’t Like Change!!!

However this class has a nasty secret: it doesn’t like change. Let’s imagine that we’re asked to update the bill_of_lading method to also print out the customer address?

class OrderReport
  def initialize(customer:, total:, address:)
    @customer = customer
    @total = total
    @address = address
  end

  def invoice
    puts "Invoice"
    puts @customer
    puts @total
  end

  def bill_of_lading
    puts "BOL"
    puts @customer
    puts "Shipping Label..."
    puts @address
  end
end

order = OrderReport.new(customer: "Google",
                        total: 100,
                        address: "123 Any Street")
order.invoice
order.bill_of_lading

This may not seem like a major change, however in order to accommodate the request we’ve had to make four changes.

One of the changes is even requiring us to pass an address to the class as a required argument. This means that even when all we need is an invoice (that doesn’t care about the customer address) we have to include the additional element.

Also of note is that this was a very small change. Imagine what would happen if we needed to build a feature such as including a QR code or something complex like that?

So not only lack scalability, it’s also breaking the open close principle. A well written class should not have to be re-written in order to integrate a new feature like having an address.

A Better Way

Thankfully we can clean up this entire class and follow the open closed principle by leveraging Object Oriented inheritance.

class OrderReport
  def initialize(customer:, total:)
    @customer = customer
    @total = total
  end
end

class Invoice < OrderReport
  def print_out
    puts "Invoice"
    puts @customer
    puts @total
  end
end

class BillOfLading < OrderReport
  def initialize(address:, **args)
    super(**args)
    @address = address
  end

  def print_out
    puts "BOL"
    puts @customer
    puts "Shipping Label..."
    puts @address
  end
end

invoice = Invoice.new(customer: "Google",
                      total: 100)

bill_of_lading = BillOfLading.new(customer: "Yahoo",
                                  total: 200,
                                  address: "123 Any Street")

invoice.print_out
bill_of_lading.print_out

In this code I’ve pulled out the invoice and bill of lading components into their own classes that inherit from the OrderReport class.

This refactor has a number of benefits:

  • It follows the open closed principle because now when we have to build new features for invoices or bill of ladings we don’t have to touch the OrderReport class. Therefore we can say that the parent class is closed.
  • Additionally, since we split up the two components, whenever we create an invoice we don’t have to pass in an unnecessary address element. This removes code that’s not needed and that could lead to confusion later on down the road.
  • Lastly our program not only follow the open closed principle, but now our classes also follow the single responsibility principle. Notice how each class has a specific focus? (aka a single responsibility)

If you run this code you’ll see that it still has the same behavior as before, but now our code is much more scalable and dare I say SOLID?