Object Composition FTW!
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
In modern PHP, you're going to spend a lot of time working with other people's classes: via external libraries that you bring into your project to get things done faster. Of course, when you do that: you can't actually edit their code if you need to change or add some behavior.
Fortunately, OO code gives us some really neat ways to deal with this limitation.
Modifying a Class without Modifying it?
For the next few minutes, I want you to pretend like our PDOShipStorage
is actually
from a third-party library. In other words, we can't modify it.
Now, let's say whenever we call fetchAllShipsData()
, it's really important for
us to log to a file, how many ships were found. But if we can't edit this file, how
can we do that?
Using Inheritance
There's actually two ways to do this, and both are pretty awesome. The first way
is to create a new class that extends PDOShipStorage
, like LoggablePDOShipStorage
,
and override some methods to add logging.
Nah, Use Composition
But forget that, let's skip to a better method called composition. First, create
a new class in the Service
directory called LoggableShipStorage
, but do not
extend PDOShipStorage
:
// ... lines 1 - 2 | |
namespace Service; | |
class LoggableShipStorage implements ShipStorageInterface | |
{ | |
// ... lines 7 - 32 | |
} |
Now, the only rule for any ship storage object is that it needs to implement the
ShipStorageInterface
. Add that, and then go to our handy "Code"->"Generate" method
to implement the 2 methods we need:
// ... lines 1 - 4 | |
class LoggableShipStorage implements ShipStorageInterface | |
{ | |
// ... lines 7 - 13 | |
public function fetchAllShipsData() | |
{ | |
// ... lines 16 - 20 | |
} | |
public function fetchSingleShipData($id) | |
{ | |
// ... line 25 | |
} | |
// ... lines 27 - 32 | |
} |
So far, this is how every ship storage starts.
But LoggableShipStorage
will not actually do any of the ship-loading work - it'll
offload all that hard work to some other ship storage object, like PDOShipStorage
.
To do that, add a new private $shipStorage
property and a public function __construct()
method that accepts one ShipStorageInterface
argument. Then, set that value onto
the $shipStorage
property:
// ... lines 1 - 4 | |
class LoggableShipStorage implements ShipStorageInterface | |
{ | |
private $shipStorage; | |
public function __construct(ShipStorageInterface $shipStorage) | |
{ | |
$this->shipStorage = $shipStorage; | |
} | |
// ... lines 13 - 32 | |
} |
For fetchAllShipData()
, just return $this->shipStorage->fetchAllShipsData()
.
Repeat for the other method: return $this->shipStorage->fetchSingleShipData()
:
// ... lines 1 - 4 | |
class LoggableShipStorage implements ShipStorageInterface | |
{ | |
// ... lines 7 - 22 | |
public function fetchSingleShipData($id) | |
{ | |
return $this->shipStorage->fetchSingleShipData($id); | |
} | |
// ... lines 27 - 32 | |
} |
We've now created a wrapper object that offloads all of the work to an internal ship storage object. This is composition: you put one object inside of another.
To use the new class, open up Container
. Inside getShipStorage()
, add
$this->shipStorage = new LoggableShipStorage()
and pass it $this->shipStorage
,
which is the PDOShipStorage
object:
// ... lines 1 - 4 | |
class Container | |
{ | |
// ... lines 7 - 51 | |
public function getShipStorage() | |
{ | |
if ($this->shipStorage === null) { | |
$this->shipStorage = new PdoShipStorage($this->getPDO()); | |
//$this->shipStorage = new JsonFileShipStorage(__DIR__.'/../../resources/ships.json'); | |
// use "composition": put the PdoShipStorage inside the LoggableShipStorage | |
$this->shipStorage = new LoggableShipStorage($this->shipStorage); | |
} | |
// ... lines 61 - 62 | |
} | |
// ... lines 64 - 75 | |
} |
We've just pulled a "fast one" on our application: our entire app thinks
we're using PDOShipStorage
, but we just changed that! If you refresh now, nothing
is different: everything still eventually goes through the PDOShipStorage
object.
But now, we have the opportunity to add more functionality - or to change functionality - in either of these methods.
Add some Logging!
To give a really simple example, replace the return statement with $ships =
and
add return $ships
below that:
// ... lines 1 - 4 | |
class LoggableShipStorage implements ShipStorageInterface | |
{ | |
// ... lines 7 - 13 | |
public function fetchAllShipsData() | |
{ | |
$ships = $this->shipStorage->fetchAllShipsData(); | |
// ... lines 17 - 19 | |
return $ships; | |
} | |
// ... lines 22 - 32 | |
} |
Between, we could call some new log()
method, passing it a string like:
just fetched %s ships
- passing that a count()
of $ships
:
// ... lines 1 - 4 | |
class LoggableShipStorage implements ShipStorageInterface | |
{ | |
// ... lines 7 - 13 | |
public function fetchAllShipsData() | |
{ | |
$ships = $this->shipStorage->fetchAllShipsData(); | |
$this->log(sprintf('Just fetched %s ships', count($ships))); | |
return $ships; | |
} | |
// ... lines 22 - 32 | |
} |
Below, add a new private function log()
with a $message
argument:
// ... lines 1 - 4 | |
class LoggableShipStorage implements ShipStorageInterface | |
{ | |
// ... lines 7 - 27 | |
private function log($message) | |
{ | |
// todo - actually log this somewhere, instead of printing! | |
echo $message; | |
} | |
} |
You should do something more intelligent in a real app, but to prove it's working, echo that message.
Let's refresh! There's our message!
Why is Composition Cool?
Wrapping one object inside of another like this is called composition. You see, when you want to change the behavior of an existing class, the first thing we always think of is
Oh, just extend that class and override some methods
But composition is another option, and it does have some subtle advantages. If
we had extended PDOShipStorage
and then later wanted to change back to our
JsonFileShipStorage
, then all of a sudden we would need to change our LoggableShipStorage
to extend JsonFileShipStorage
. But with composition, our wrapper class can work
with any ShipStorageInterface
. We could change just one line to go back to loading
files from JSON and not lose our logging.
This isn't always a ground-breaking difference, but this is what people mean when they talk about "composition over inheritance".
Alright guys! I have tried to think of all the weird stuff that we haven't talked about with object oriented coding, and I've run out! You are now super qualified with this stuff - so get out there, find some classes, find some interfaces, make some traits, do some good, and just keep practicing. It's going to sink in more and more over time, and serve you for years to come, in many different languages.
See you next time!
Thank you for this series of tutorials. This has done wonders for my understanding of OO PHP. Truly helpful!