转自 Symfony-Project 的创始人:Fabien Potencier。
查了下中文,貌似应该翻译成 “依赖注入”?
This article is the first of a series on Dependency Injection in general and the implementation of a Dependency Injection Container in PHP.
Today, I won’t talk about the container yet as I first want to introduce the concept of Dependency Injection with some concrete examples that will hopefully demonstrate the problems it tries to solve and the benefits it gives to the developer. If you already knows the concept of Dependency Injection, you can safely skip this article and instead wait for the next one.
Dependency Injection is probably one of the most dead simple design pattern I know. And odds are you have probably already used Dependency Injection. But it is also one of the most difficult one to explain well. I think it is partly due to the nonsense examples used in most introductions to Dependency Injection. I have tried to come up with examples that fits the PHP world better. As PHP is a language mainly used for web development, let’s take a simple Web example.
To overcome the statelessness of the HTTP protocol, web applications need a way to store user information between web requests. This is of course quite simple to achieve by using a cookie, or even better, by using the built-in PHP session mechanism:
$_SESSION['language'] = 'fr';
The above code stores the user language in the
session variable. So, for all subsequent requests of the same user, the
will be available in the global
array:
$user_language = $_SESSION['language'];
As Dependency Injection only makes sense in an Object-Oriented world, let’s pretend we have a
class that wraps the PHP session mechanism:
class SessionStorage { function __construct($cookieName = 'PHP_SESS_ID') { session_name($cookieName); session_start(); } function set($key, $value) { $_SESSION[$key] = $value; } function get($key) { return $_SESSION[$key]; } // ... }
… and a
class that provides a nice high-level interface:
class User { protected $storage; function __construct() { $this->storage = new SessionStorage(); } function setLanguage($language) { $this->storage->set('language', $language); } function getLanguage() { return $this->storage->get('language'); } // ... }
Those classes are simple enough and using the
class is also rather easy:
$user = new User(); $user->setLanguage('fr'); $user_language = $user->getLanguage();
All is good and well… until you want more flexibility. What if you want to change the session cookie name for instance? Here are some random possibilities:
- Hardcode the name in the
User
class in the
SessionStorageconstructor:
class User { function __construct() { $this->storage = new SessionStorage('SESSION_ID'); } // ... }
- Define a constant outside of the
User
class:
class User { function __construct() { $this->storage = new SessionStorage(STORAGE_SESSION_NAME); } // ... } define('STORAGE_SESSION_NAME', 'SESSION_ID');
- Add the session name as a
User
constructor argument:
class User { function __construct($sessionName) { $this->storage = new SessionStorage($sessionName); } // ... } $user = new User('SESSION_ID');
- Add an array of options for the storage class:
class User { function __construct($storageOptions) { $this->storage = new SessionStorage($storageOptions['session_name']);
} // ... } $user = new User(array('session_name' => 'SESSION_ID'));
All these alternatives are quite bad. Hardcoding the session name in the
class does not really solve the problem as you cannot easily change your mind later on without changing the
class again. Using a constant is also a bad idea as the
class now depends on a constant to be set. Passing the session name as an argument or as an array of options is probably the best solution, but it still smells bad. It clutters the
constructor arguments with things that are not relevant to the object itself.
But there is yet another problem that cannot be solved easily: How can I change the
class? For instance, to replace it with a mock object to ease testing. Or perhaps because you want to store the sessions in a database table or in memory. That’s impossible with the current implementation, except if you change the
class.
Enter Dependency Injection. Instead of creating the
object inside the
class, let’s inject the
object in the
object by passing it as a constructor argument:
class User { function __construct($storage) { $this->storage = $storage; } // ... }
That’s Dependency Injection. Nothing more! Using the
class is now a bit more involving as you first need to create the
object:
$storage = new SessionStorage('SESSION_ID'); $user = new User($storage);
Now, configuring the session storage object is dead simple, and replacing the session storage class is also very easy. And everything is possible without changing the
class thanks to the better separation of concerns.
The Pico Container website describes Dependency Injection like this:
“Dependency Injection is where components are given their dependencies through their constructors, methods, or directly into fields.”
As any other design pattern, Dependency Injection also has some anti-patterns. The Pico Container website describes some of them.
Dependency Injection is not restricted to constructor injection:
- Constructor Injection:
class User { function __construct($storage) { $this->storage = $storage; } // ... }
- Setter Injection:
class User { function setSessionStorage($storage) { $this->storage = $storage; } // ... }
- Property Injection:
class User { public $sessionStorage; } $user->sessionStorage = $storage;
As a rule of thumb, constructor injection is best for required dependencies, like in our example, and setter injection is best for optional dependencies, like a cache object for instance.
Nowadays, most modern PHP frameworks heavily use Dependency Injection to provide a set of decoupled but cohesive components:
// symfony: A constructor injection example $dispatcher = new sfEventDispatcher(); $storage = new sfMySQLSessionStorage( array( 'database' => 'session', 'db_table' => 'session')); $user = new sfUser($dispatcher, $storage, array('default_culture' => 'en'));
// Zend Framework: A setter injection example $transport = new Zend_Mail_Transport_Smtp('smtp.gmail.com', array( 'auth' => 'login', 'username' => 'foo', 'password' => 'bar', 'ssl' => 'ssl', 'port' => 465, )); $mailer = new Zend_Mail(); $mailer->setDefaultTransport($transport);
If you are interested in learning more about Dependency Injection, I highly recommend you read the Martin Fowler introduction or the excellent Jeff More presentation. You can also have a look at a presentation I gave last year on Dependency Injection, where I talk in more detail on the example I have talked about in this article.
That’s all for today. I hope you now have a better understanding of the Dependency Injection concept. In the next installment of this series, I will talk about Dependency Injection Containers.