- Read Tutorial
- Watch Guide Video
Continuing on our discussion of SOLID development principles, in this guide I’m going to walk through the: open closed principle.
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?