Polymorphism in PHP
Making your PHP more easy to use and maintain through implementing and depending on an interface
Written by Anthony Chambers , and read8,817 times
In my recent post recommending that you don't inject a dependency injection container, I touched upon a topic that isn't so well known by many PHP developers; Polymorphism.
In object oriented programming, polymorphism is one of the most important concepts. We ensure that our classes are grouped into a sensible structure and we give all of these related objects the same interface. That is, they all have the same methods available and take the same arguments. Why is this important? Because as long as this interface is implemented then it means that we don't need to care about how these classes work, we just know how to work with them because of said interface.
This by itself doesn't make our code polymorphic, it just makes it sensible and easy to work with. However, making the code that will actually consume these objects depend on their interface, rather than the specific type, we make our code polymorphic. This goes hand in hand with encapsulation.
Let's take a simple example; image formats.
For our demonstration, we only need our image objects to do a few things; open, read and close the file. So we have a really simple interface for these:
<?php interface Image { public function __construct($filename); public function close(); public function getData(); }
That's pretty simple, right? We will pass in the filename when we create new object, and when we're done we'll call the close()
method. We can also read the data.
But it doesn't do anything, right? It doesn't tell us how it should work internally, but that's the point; we don't care. And in the same way, when you're using one of these objects in your code, you don't care what happens internally either. All you need to know is that you open the file by providing a filename to the constructor, and when you're done you call close()
. What else would you need to know?
So then we have the actual implementations. Here are a couple of examples for two image formats; PNG and JPG:
<?php class PNG implements Image { protected $data, $handle; public function __construct($filename) { $this->handle = fopen($filename, "r"); $this->data = fread($this->handle, filesize($filename)); } public function close() { fclose($this->handle); } public function getData() { return $this->data; } } class JPG implements Image { protected $filename; public function __construct($filename) { $this->filename = $filename; } public function close(){} public function getData() { $handle = fopen($this->filename); $data = fread($handle, filesize($this->filename)); fclose($handle); return $data; } }
To be clear, I'm not recommending that you use these exact examples as they're pretty bad at being image objects, but bear with me here.
So, the first example is for a PNG file. I pass in the filename when I instantiate my object and internally it will create a file handle and read the data in to a property and store it. When I call getData()
it will just return that data. When I close()
the object it will close the handle. Pretty simple stuff.
Then we come on to the JPG version. I instantiate my object EXACTLY the same way by passing it in my filename. Only this time it doesn't create a file handle, and it doesn't read in any data. When I call my getData()
method, however, it will create the handle and it will read the data in and return it immediately, never storing it within the object. It also closes the handle immediately, assuming I won't need it again. My close()
method does nothing at all, but it IS implemented.
So, the two objects work quite differently internally. The PNG one will most likely eat a lot more memory than the JPG one because it keeps the data internally, whether I ever require it or not. The JPG one will probably be slower though if I need to get the output more than once because it has to get a handle and read in the data whenever I want to get the data back.
If they work differently, you'd assume that I would need to work around this when I'm using these objects, right?
Wrong!
Because they implement the same interface, I use them both exactly the same way:
<?php class ViewImage { protected $image; public function __construct(Image $image) { $this->image = $image; } public function view() { $data = $this->image->getData(); $this->image->close(); return $data; } } $pngImg = new PNG('path/to/image.png'); $pngViewer = new ViewImage($pngImg); echo $pngViewer->view(); $jpgImg = new JPG('path/to/image.jpg'); $jpgViewer = new ViewImage($jpgImg); echo $jpgViewer->view();
Again, this code is simply to demonstrate a concept, please don't use it!
Notice in my ViewImage
class that it has only one dependency; the Image
interface. It will work with ANY object whose class implements this interface. It knows that it has getData()
and close()
methods, so it will call them and just work, despite the fact that they're very different internally.
You can also see that the way that I use my PNG
and JPG
objects are also identical; I instantiate both by simply passing in a filename, and I just pass them to my ViewImage
object in the constructor. That's all.
I can also very easily add support for another image format, say BMP, TIFF or GIF. All I need to do is to implement the same interface and regardless of what my actual implementation looks like, it will work fine with my ViewImage
class because it only needs an object that implements the Image
interface.
The combination of implementing the interface in my classes, and then depending only on that interface, that's what makes this polymorphic, and in making it polymorphic I've made it incredibly easy to add more image types, and I've made it very easy to see how I can use these different objects.
I've added simplicity, and flexibility, and as developers isn't that exactly what we're looking for?