I'll preface the long question with the short version of the question:
Short version of question
What is wrong with allowing an object to instantiate its own dependencies, and then providing constructor arguments (or setter methods) to simply override the default instantiations?
class House
{
protected $door;
protected $window;
protected $roof;
public function __construct(IDoor $door = null, IWindow $window = null, IRoof $roof = null)
{
$this->door = ($door) ? $door : new Door;
$this->window = ($window) ? $window : new Window;
$this->roof = ($roof) ? $roof : new Roof;
}
}
Long version of question
My motivation for this question is that dependency injection requires you to jump through hoops just to give an object what it needs. IoC containers, factories, service locators..... all of these introduce lots of additional classes and abstractions that complicate the API of your application, and I would argue, make testing just as difficult in many cases.
Isn't it logical that an object does in fact know what dependencies it needs in order to function properly???
If the two primary motivations of dependency injections are code re-usability, and unit testability, then being able to override default instantiations with stubs or other objects accomplishes that just fine.
Meanwhile, if you need to add a House class to your application, you ONLY need to code the House class, and not a factory and/or a DI container on top of it. Further, any client code that makes use of the house can just include the house, and doesn't need to be given a house factory or abstract service locator from somewhere up above. Everything becomes extremely straight-forward, with no middleman code, and instantiated only when it's needed.
Am I totally out of line in thinking that if an object has dependencies, it should be able to load them on its own, while providing a mechanism for those dependencies to be overloaded if desired?
Example
#index.php (front controller)
$db = new PDO(...);
$cache = new Cache($dbGateway);
$session = new Session($dbGateway);
$router = new Router;
$router::route('/some/route', function() use ($db, $cache, $session)
{
$controller = new SomeController($db, $cache, $session);
$controller->doSomeAction();
});
#SomeController.php
class SomeController
{
protected $db;
protected $cache;
protected $session;
public function __construct(PDO $db, ICache $cache, ISession $session)
{
$this->db = $db;
$this->cache = $cache;
$this->session = $session;
}
public function doSomeAction()
{
$user = new \Domain\User;
$userData = new \Data\User($this->db);
$user->setName('Derp');
$userData->save($user);
}
}
Now, in a very large application with many different models/data classes and controllers, I feel like having to pass the DB object THROUGH every single controller (which won't need it) just to give it to every data mapper (which will need it), is a bit smelly.
And by extension, passing a service locator or DI container through the controller, just to locate the database to then give it to the datamapper every single time, also seems a bit smelly.
Same goes for passing a factory or abstract factory through to the controller, and then having to instantiate new objects through something cumbersome like $this->factory->make('\Data\User');
seems awkward. Especially since you need to code the abstract factory class, then the actual factory that wires up the dependencies for the object you want.
An alternative to dependency injection is using a service locator. The service locator design pattern also improves decoupling of classes from concrete dependencies. You create a class known as the service locator that creates and stores dependencies and then provides those dependencies on demand.
Basically, dependency injection makes some (usually but not always valid) assumptions about the nature of your objects. If those are wrong, DI may not be the best solution: First, most basically, DI assumes that tight coupling of object implementations is ALWAYS bad.
If you have a really small project with 12 classes, then a DI framework is almost certainly overkill. As a rule of thumb, the point where it becomes truly useful is when you find yourself repeatedly writing code that wires up object graphs with multiple dependencies and have to think about where to put that code.
Spring IoC (Inversion of Control) Container is the core of Spring Framework. It creates the objects, configures and assembles their dependencies, manages their entire life cycle. The Container uses Dependency Injection(DI) to manage the components that make up the application.
A good antonym for dependency injection is hard coding a dependency.
Your question in nicely asked and I really like people questioning stuff that is common sense for reasons of 'unit testing and maintainability' (no matter which of these You're-a-bad-programmer-if-you-don't-do-it-topics, it's always about unit testing and maintainability). So you're asking the right question here: Does DI really support unit testing and maintainability and, if yes, how? And to anticipate it: It does if used correctly...
About Decomposition
Dependency Injection (DI) and Inversion of Control (IoC) are mechanism, which enhance the core concepts of encapsulation and separation of concerns of OOP. So, to answer the question, it has to be argued why encapsulation and separation of concerns are cool things to have. Both are the core mechanisms of decomposition: Encapsulation (Yes, we have modules) and separation of concerns (and we have modules in a way it makes sense). A lot could be written about this topic, but, for now, it must be sufficient to say that it is about reducing complexity. Decomposition of a system allows you to break down a system - no matter how big - into chunks that a human brain is able to manage. Although a little philosophical, that's really important: If there weren't limitations of the human brain, the whole maintainability topic wouldn't be that important. Ok, so let's say: Decomposition is a trick to reduce the perceived complexity of a system into chunks that we can manage.
But, as always, it comes at a cost: Decomposition also adds complexity, as you have said with regards to DI. So does it still make sense? Yes, because:
The artificially added complexity is independent of the inherent complexity of the system.
That's basically it, on an abstract level. And it has implications: You need to choose the degree of decomposition and the effort which you spend to achieve it, according to the inherent complexity of the system you're building (or the complexity it can reach some day).
Decomposition with DI
Regarding DI particularly: According to the above, there are sufficiently small systems, where the added complexity of DI does not justify the reduced perceived complexity. And, unfortunately, every single tutorial on the web deals with one of these which doesn't support understanding what the whole fuzz is about.
However, most (or many, at least) real-life projects reach a degree of inherent complexity that the investment in additional decomposition is well spent, because the reduction of perceived complexity speeds up subsequent development and reduces mistakes. And dependency injection is one of the techniques to do so:
DI supports separation of the What (interface) and of the How (implementation): If it's only about glass doors, I agree: If that's too much for one's brain, he or she probably shouldn't be a programmer. But things are more complex in real-life: DI allows you to focus on what is really important: As a house, I don't care about my door as long as I can rely on the fact that it can be closed and opened. Maybe there aren't any doors existing right now? You simply don't need to care at this point. When registering the components in your container, you can focus again: What door do I want in my house? You don't need to care about the door or the house itself anymore: They are fine, you know already. You've separated concerns: The definition of how things go together (components) and actually putting them together (container). That's all, as far as I can tell from my experience. It sounds clumsy, but in real-life, it is a great achievement.
A little less philosophical
To get it down to earth again, a number of more practical advantages:
While a system is evolving, there are always parts which have not yet been developed. Specifying a behavior is far less work than implementing it, in most cases. Without DI, you cannot develop your house as long as no door has been developed, because there is nothing to instantiate. With DI, you don't care: You design your house, just with the interfaces, you write tests with mocks for these interfaces and your fine: Your house works, without windows and doors even existing.
You probably know the following: You've worked for days on something (lets say a glass door) and you're proud. Six months later - you've learned a lot in the meantime - you look at it again, and it's crap. You throw it away. Without DI, you need to change your house, because it uses the class you've just trashed. With DI, your house doesn't change. It might sit in it's own assembly: You don't even need to recompile the house assembly, it's not touched. This, in a complex scenario, is a huge advantage.
There is more, but maybe with all of this in mind, it becomes easier to imagine the benefits of DI when you read about them next time...
While another answers are good, I'll try to approach this question from practical point of view.
Imagine, you have a Content Management System, where you can tweak its configuration as you wish. Suppose, this configuration is stored in a database. And since then, it implies that you should have instantiated the thing like:
$dsn = '....';
$pdo = new PDO($dsn, $params);
$config_adapter = new MySQL_Config_Adapter($pdo);
$config_manager = new Config_Manager($config_adapter);
// $config_manager is ready to be used
class Foo
{
public function __construct($config = null)
{
if ($config !== null) {
global $pdo;
$config_adapter = new MySQL_Config_Adapter($pdo);
$config_manager = new Config_Manager($config_adapter);
$this->config = $config_manager;
} else {
// Ok, it was injected
$this->config = $config;
}
}
}
There are 3 obvious problems here:
So, you basically decide if you want this to have a global state or not. If you provide a $config
instance, then you're saying that you don't want a global state. Otherwise you're saying that you do want this.
So, what if you decide to switch from MySQL
to MongoDB
, or even plain file-based PHP-array
to store CMS's
configuration? Then you would have to rewrite a lot of code, that is responsible for dependency initialization.
A class should have only one reason to change. A class should serve only singular purpose.
That means, the Foo
class has more than one responsibility - it's also responsible for dependency management.
public function __construct(IConfig $config)
{
$this->config = $config;
}
Because it isn't tightly-coupled to particular adapter, and since then it would be so easy to unit-test that, or replace an adapter (Say, MySQL with something else)
If you're overriding default objects
, then you're doing something wrong, and that is a sign that your class doing too much.
A basic purpose of constructors is to initialize the state of a class. If you initialized the state, and then you're altering that state via dependency setter methods, then you end up with broken encapsulation, which states An object should be in complete control of its state and implementation
Let's look at your code example.
public function __construct(IDoor $door = null, IWindow $window = null, IRoof $roof = null)
{
$this->door = ($door) ? $door : new Door;
$this->window = ($window) ? $window : new Window;
$this->roof = ($roof) ? $roof : new Roof;
}
Here you're saying, something like this : if some of the arguments aren't provided, then import an instance of that argument from global scope. The problem here is that your House
knows where you dependencies would come from, while it should be completely unaware of such information.
Now let's raise some real-world scenario questions:
If you're going to stick with the way you wrote the code, then you end up with mass code duplication. With "pure" DI in mind, this will be as simple as:
$door = new Door();
$door->setColor('black');
$window = new Window();
$window->setSize(500, 500);
$a_house = new House($door, $window, $roof);
// As I said, I want house2 to have the same door, but different window size
$window->setSize(1000, 1000);
$b_house = new House($door, $window, $roof);
AGAIN : The core point of Dependency Injection is that objects can share the same instances
One more thing,
Service Locators/IoC containers are responsible for objects storage. They simply store/retrieve objects, like $pdo
.
Factories simply abstract an instantiation of a class.
So that, They aren't "part" of Dependency Injection, they take advantage of it.
That's it.
A problem with doing things like this comes when your dependencies also have dependencies that must be specified. Then your constructor needs to know how to construct its dependencies and then your constructor starts becoming very complicated.
Using your example:
The Roof
object requires a pitch angle. The default angle depends on the location of your house (a flat roof doesn't work so good with 10' of snow) by new/changes to the business rules. So now your House
is needing to calculate which angle to pass in to the Roof
. You can do this either by passing the location (which House
currently only needs for calculating the angle or it is creating a "default" location to pass in the Roof
constructor). Either way, the constructor now has to do some work to create the default roof.
This can happen with any of your dependencies, once one of them requires something to be determined/calculated then your object has to know about its dependencies and how to make them. Something that it shouldn't have to do.
This won't necessarily happen in every case and in some cases you would be able to do get away with what you are suggesting. However you are taking a risk.
Trying to make things "easier" for people can lead to your design becoming inflexible and difficult as the code needs to be changed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With