Dependency Injection
Anthony Ferrara has a brilliantly simple youtube video explaining dependency injection that I highly recommend.
In basic terms, anywhere that you pass an object into another Class's constructor, rather than creating a new instance of that object within the constructor, you are using dependency injection. Dependency injection allows for high levels of abstraction, which makes your codebase easier to read and understand, whilst also being more flexible and thus, easier to maintain.
An Example
A good example of using dependency injection is for a "storage driver" for a web application. You could start building your web application immediately without knowing how you are actually going to store data in future, and leave that up to other people to provide later. Below is an example where a StorageDriver
object is a dependency injected into a MyWebApplication
object.
Interface StorageDriver
{
public function save($data){} // returns an integer ID
public function load($id)
}
class MyWebApplication
{
private $m_driver;
private $m_data;
public function __construct(StorageInterface $driver)
{
$this->m_driver = $driver;
$this->m_data = "";
}
public function sayHello()
{
$this->m_data .= "hello world";
}
public function save()
{
$this->m_driver->save($this->m_data);
}
public function load($id)
{
$this->m_driver->load($id);
}
...
}
Now I know precisely how my web application is going to work, even if I have no clue about the how to actually store the data, such as by writing SQL queries or writing to files. However, an even better aspect is that your web application is now "future-proof" and can work on almost anything people can think of down the line, such as saving to ping packets. You may start out by writing a database driver but later switch to using Amazon S3 simply by swapping out 1 line of code:
// $driver = new DatabaseDriver(); // old way
$driver = new AmazonS3Driver(); // new way
$app = new MyWebApplication($driver);
Dependency injection can often be used as more preferable way to achieve the same goal that people use inheritance for. An example of someone trying to achieve the previous effect with inheritance is this:
Interface StorageDriver
{
public function save($data){} // returns an integer ID
public function load($id)
}
abstract class AbstractDatabaseWebApplication implements StorageDriver
{
protected $m_data;
public function save()
{
// write an sql query here to save the data
$mysqli->query("INSERT INTO....");
}
public function load($id)
{
// write an sql query here to load the data
$mysqli->query("SELECT * FROM...");
}
...
}
abstract class AbstractFileWebApplication implements StorageDriver
{
protected $m_data;
public function save()
{
// write $m_data to file here
}
public function load($id)
{
// read from a file here
$this->m_data = ...
}
...
}
/*
* Changing over to using a file based storage system
* requires me to change this class so it extends
* AbstractFileWebApplication instead of AbstractDatabaseWebApplication.
* There are many things wrong with this plan...
*/
class MyWebApplication extends AbstractDatabaseWebApplication
{
// m_data is declared in the parents...
public function __construct()
{
$this->m_data = "";
}
public function sayHello()
{
$this->m_data .= "hello world";
}
// save and load are available from parent class so
// dont override them here....
...
}
The Problem With This Inheritance-based Alternative
Hopefully I don't need to explain all the things that could possibly go wrong with this inheritance based approach when working in a team, but I'll describe the top two most likely. Other developers will take shortcuts and just add methods to whatever parent "driver" class is currently being used (e.g. AbstractDatabaseWebApplication
). The testers and pointy-haired-managers will see that the application works and sign off on the work. Other developers may then make use of that method, either in the MyWebApplication class, or even worse, outside of it if the method was public
. Then when MyWebApplication is changed to extend AbstractFileWebApplication
instead, the codebase will fall over.
Another likely scenario is that someone decides that save() and load() are too slow or don't store/retrieve the extra information they need, so they override the methods which also causes the codebase to not work when switched over.
First published: 16th August 2018