非常不錯:http://www.martinfowler.com/
Chances are, at some point in your learning, you've come across the term, "dependency injection." If you're still relatively early in your learning, you likely formed a confused expression and skipped over that part. Still, this is an important aspect of writing maintainable (and testable) code. In this article, I'll explain it in as simple a way as I'm capable of.
An Example
Let's jump into a fairly generic piece of code, and discuss its short-comings.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class Photo { /** * @var PDO The connection to the database */ protected $db ; /** * Construct. */ public function __construct() { $this ->db = DB::getInstance(); } } |
At first glance, this code might seem fairly harmless. But consider the fact that, already, we've hardcoded a dependency: the database connection. What if we want to introduce a different persistence layer? Or, think about it this way: why should the Photo
object be communicating with outside sources? Doesn't that violate the concept of separation of concerns? It certainly does. This object shouldn't be concerned with anything that isn't directly related to a Photo
.
The basic idea is that your classes should be responsible for one thing only. With that in mind, it shouldn't be responsible for connecting to databases and other things of that nature.
Let's regain control of the class, and, instead, pass in the database connection. There are two ways to accomplish this: constructor and setter injection, respectively. Here's examples of both:
Constructor Injection
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
class Photo { /** * @var PDO The connection to the database */ protected $db ; /** * Construct. * @param PDO $db_conn The database connection */ public function __construct( $dbConn ) { $this ->db = $dbConn ; } } $photo = new Photo( $dbConn ); |
Above, we're injecting all required dependencies, when the class's constructor method runs, rather than hardcoding them directly into the class.
Setter Injection
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
class Photo { /** * @var PDO The connection to the database */ protected $db ; public function __construct() {} /** * Sets the database connection * @param PDO $dbConn The connection to the database. */ public function setDB( $dbConn ) { $this ->db = $dbConn ; } } $photo = new Photo; $photo ->setDB( $dbConn ); |
With this simple change, the class is no longer dependent upon any specific connection. The outside system retains complete control, as should be the case. While it may not be immediately visible, this technique also makes the class considerably easier to test, as we can now mock the database, when calling the setDB
method.
Even better, if we later decide to use a different form of persistence, thanks to dependency injection, it's a cinch.
"Dependency Injection is where components are given their dependencies through their constructors, methods, or directly into fields."
The Rub
There's one problem with using setter injection in this way: it makes the class considerably more difficult to work with. The user now must be fully aware of the class's dependencies, and must remember to set them, accordingly. Consider, down the line, when our fictional class requires a couple more dependencies. Well, following the rules of the dependency injection pattern, we'd have to do:
1
2
3
4
|
$photo = new Photo; $photo ->setDB( $dbConn ); $photo ->setConfig( $config ); $photo ->setResponse( $response ); |
Yikes; the class may be more modular, but we've also piled on confusion and complexity. Before, the user could simply create a new instance of Photo
, but, now, he has to remember to set all of these dependencies. What a pain!
The Solution
The solution to this dilemma is to create a container class that will handle the brunt of the work for us. If you've ever come across the term, "Inversion of Control (IoC)," now you know what they're referring to.
This class will store a registry of all the dependencies for our project. Each key will have an associated lambda function that instantiates the associated class.
There are a couple ways to tackle this. We could be explicit, and store methods, such as newPhoto
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// Also frequently called "Container" class IoC { /** * @var PDO The connection to the database */ protected $db ; /** * Create a new instance of Photo and set dependencies. */ public static newPhoto() { $photo = new Photo; $photo ->setDB( static :: $db ); // $photo->setConfig(); // $photo->setResponse(); return $photo ; } } $photo = IoC::newPhoto(); |
Now, $photo
will be equal to a new instance of the Photo
class, with all of the required dependencies set. This way, the user doesn't have to remember to set these dependencies manually; he simply calls thenewPhoto
method.
The second option, rather than creating a new method for each class instantiation, is to write a generic registry container, like so:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
class IoC { /** * @var PDO The connection to the database */ protected static $registry = array (); /** * Add a new resolver to the registry array. * @param string $name The id * @param object $resolve Closure that creates instance * @return void */ public static function register( $name , Closure $resolve ) { static :: $registry [ $name ] = $resolve ; } /** * Create the instance * @param string $name The id * @return mixed */ public static function resolve( $name ) { if ( static ::registered( $name ) ) { $name = static :: $registry [ $name ]; return $name (); } throw new Exception( 'Nothing registered with that name, fool.' ); } /** * Determine whether the id is registered * @param string $name The id * @return bool Whether to id exists or not */ public static function registered( $name ) { return array_key_exists ( $name , static :: $registry ); } } |
Don't let this code scare you; it's really very simple. When the user calls the IoC::register
method, they're adding an id, such as photo
, and its associated resolver, which is just a lambda that creates the instance and sets any necessary dependencies on the class.
Now, we can register and resolve dependencies through the IoC
class, like this:
01
02
03
04
05
06
07
08
09
10
11
|
// Add `photo` to the registry array, along with a resolver IoC::register( 'photo' , function () { $photo = new Photo; $photo ->setDB( '...' ); $photo ->setConfig( '...' ); return $photo ; }); // Fetch new photo instance with dependencies set $photo = IoC::resolve( 'photo' ); |
So, we can observe that, with this pattern, we're not manually instantiating the class. Instead, we register it with the IoC
container, and then request a specific instance. This reduces the complexity that we introduced, when we stripped the hardcoded dependencies out of the class.
1
2
3
4
5
|
// Before $photo = new Photo; // After $photo = IoC::resolve( 'photo' ); |
Virtually the same number of characters, but, now, the class is significantly more flexible and testable. In real-world usage, you'd likely want to extend this class to allow for the creation of singletons as well.
Embracing Magic Methods
If we want to reduce the length of the IoC
class even further, we can take advantage of magic methods - namely __set()
and __get()
, which will be triggered if the user calls a method that does not exist in the class.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class IoC { protected $registry = array (); public function __set( $name , $resolver ) { $this ->registry[ $name ] = $resolver ; } public function __get( $name ) { return $this ->registry[ $name ](); } } |
Popularized by Fabien Potencier, this is a super-minimal implementation - but it'll work. Whether or not__get()
or set()
runs will be dependent upon whether the user is setting a value or not.
Basic usage would be:
01
02
03
04
05
06
07
08
09
10
11
|
$c = new IoC; $c ->mailer = function () { $m = new Mailer; // create new instance of mailer // set creds, etc. return $m ; }; // Fetch, boy $mailer = $c ->mailer; // mailer instance |
Thanks for reading!