The Open Closed Principle is another concept in our study of the solid design approaches in the object oriented style. You may have heard the idea that code should be open for extension but closed for modification. Ok. Really what this means is that when writing code, we should aim for the ability to change behavior without having to actually change the code. Another way of saying this is to program in a way so that the code does not need to be changed each time the requirements of the project change. Like most things solid, it sounds great, and is far easier said than done. It is a worthy principle to investigate however.
No Area Calculations
Pretty much every tutorial under the sun uses the example of calculating the area of different shapes to illustrate the open closed principle. Let’s not do that. We need to create something from our imagination if we really want the concept to stick. We will take the concept of delivering Pizzas. Sounds much more fun than calculating the area of shapes. Instead of dealing with how to calculate an area, we’ll work on how to deliver a Pizza. As such, we need a PizzaDelivery
class.
<?php
class PizzaDelivery
{
public function deliver()
{
$prepare = 'place pizza in car';
$travel = ' and drive down the road';
$customer = ' then give pizza to happy customer';
return $prepare.$travel.$customer;
}
}
$pizza = new PizzaDelivery;
echo $pizza->deliver();
// place pizza in car and drive down the road then give pizza to happy customer
Here we have a perfectly good class that is able to deliver Pizza in a fine manner. It turns out however, that your Pizza Job is going above and beyond. Not only will they drive pizza across town, if a customer lives on an island, this Pizza Restaurant will Bring it to them right across the water. Now you are the Pizza delivery guy. Is your deliver method going to work right when you need to travel by water? It might go something like this.
$pizza = new PizzaDelivery;
echo $pizza->deliver();
// Sorry you just drove into the Atlanic Ocean. You're dead.
Breaking The OCP
Fair enough, we can fix this. Let’s just change our code to allow for delivering Pizza by boat.
<?php
class Car {}
class Boat {}
class PizzaDelivery
{
public function deliver($transporter)
{
if(is_a($transporter, 'Car'))
{
$prepare = 'place pizza in car';
$travel = ' and drive down the road';
$customer = ' then give pizza to happy customer';
return $prepare.$travel.$customer;
}
if(is_a($transporter, 'Boat'))
{
$prepare = 'place pizza in Boat';
$travel = ' and sail across the bay';
$customer = ' then give pizza to happy customer on Nantucket';
return $prepare.$travel.$customer;
}
}
}
$pizza = new PizzaDelivery;
echo $pizza->deliver(new Boat);
// place pizza in Boat and sail across the bay then give pizza to happy customer on Nantucket
Nice! Just like that, you are now able to deliver Pizza by boat! You got the Pizza to the customer on time, she was so thrilled that she invited you in to watch Breaking Bad. Before you go popping the champagne however, you need to know, you just broke the open closed principle. How so? Well, when your Pizza shop decided to implement a new behavior, you had to go back and change the original code to fit that need. In addition, the dead ringer here is that you are doing type checking inside the class to decide which behavior to take. Our PizzaDelivery class should be able to simply call a deliver method, and get the job done, whether by Car, Boat, or Plane! Let’s clean things up (or dry up, make solid, remove smell, whatever you want to call it). Our goal is to simply have a method call something like this:
$pizza->deliver();
and our Pizza will be delivered, no matter how or where it gets delivered to. We can do this!
<?php
interface Deliverable
{
public function deliver();
}
class Car implements Deliverable
{
public function deliver()
{
$prepare = 'place pizza in car';
$travel = ' and drive down the road';
$customer = ' then give pizza to happy customer';
return $prepare . $travel . $customer;
}
}
class Boat implements Deliverable
{
public function deliver()
{
$prepare = 'place pizza in Boat';
$travel = ' and sail across the bay';
$customer = ' then give pizza to happy customer on Nantucket';
return $prepare . $travel . $customer;
}
}
class PizzaDelivery
{
protected $transporter;
public function __construct(Deliverable $transporter)
{
$this->transporter = $transporter;
}
public function deliver()
{
return $this->transporter->deliver();
}
}
// Delivery by Boat works
$pizza = new PizzaDelivery(new Boat);
echo $pizza->deliver();
// place pizza in Boat and sail across the bay then give pizza to happy customer on Nantucket
// Deliver by Car works
$pizza = new PizzaDelivery(new Car);
echo $pizza->deliver();
// place pizza in car and drive down the road then give pizza to happy customer
So what did we do here? We removed the need to do type checking inside our PizzaDelivery class. By doing so, we can change behaviors in the application, without touching the code. We are following the convention of closed for modification and open for extension. Here is the process we followed to achieve this.
- Extract the behavior for the
deliver()
method out of thePizzaDelivery
class - Create an interface
Deliverable
which defines the contract for delivery - Create new classes as needed that implement the interface to add behavior
Open For Extension
Our code is now open for extension! To prove this, we have decided that we will now offer the ability to deliver Pizza to the moon! Watch us add this behavior without ever touching the PizzaDelivery
class.
Add (do not modify!) to the codebase
class Spaceship implements Deliverable
{
public function deliver()
{
$prepare = 'place pizza in Spacechip';
$travel = ' blast off in 10, 9, 8, .. LAUNCH! ';
$customer = ' then give pizza to happy space martian on the moon';
return $prepare . $travel . $customer;
}
}
Observe success of new code
$pizza = new PizzaDelivery(new Spaceship);
echo $pizza->deliver();
// place pizza in Spacechip blast off in 10, 9, 8, .. LAUNCH! then give pizza to happy space martian on the moon
With our interface in place, we were able to leave our PizzaDelivery
class, as well as our client code, untouched. All we had to do was add a new Spaceship
class and have it implement our Deliverable
interface. Done. 🙂