Reputation: 1414
I have two classes
ClassA , ClassB
Classes commonly depend upon two basic services and repositories
ServiceA , ServiceB
Classes (ClassA , ClassB
) use DI principle to inject dependency using constructor.
Since all three share a few common service as mentioned above I want to group all the common methods and services to a class Base
Like this
Base Class
class Base {
protected $A;
protected $B;
public function __construct(ServiceA $A, ServiceB $B){
$this->A = $A;
$this->B = $B;
}
}
Child Service
class Child extends Base {
protected $C;
public function __construct(ChildDependency $C){
$this->C = $C;
}
public function doStuff()
{
//access the $A (**Its null**)
var_dump($this->A);
}
}
Question
How can I have common parent dependency without breaking IoC principles?
Possible Case 1
I know I have to call parent::__construct()
to initialize Base constructor. But then I have to define Parent's dependency in all child class like below.
(But for large number of child I have to repeat this process. It defeats purpose of having common DI point).
class Child extends Base {
protected $C;
public function __construct(ChildDependency $C, ParentDepen $A, ParentDepn $B){
parent::_contruct($A,$B);
$this->C = $C;
}
}
Possible Case 2
Having use Getter and Setter. But I think they break the IoC principle.
Upvotes: 5
Views: 2227
Reputation: 9135
I'm not sure I understand what you are trying to accomplish by using inheritance. Inheritance should be used for things conceptually the same type, not for a common dependency injection point. If you have a natural case for using inheritance, and you want to use dependency injection as well, in order to follow good practices, then that makes sense. But if inheritance and constructor parameters are the way to go, you can't get around having to change them in lots of classes if you have lots of inheritance. But having lots of inheritance is usually a sign you are overusing inheritance. Inheritance is often overused. It makes the behavior of classes complex and difficult to read and change. Always look for an alternative to inheritance and only use it when it works better than the alternatives.
You have another code smell. If you need to pass enough constructor parameters that this is creating maintainability issues, then your classes are probably violating the single responsibility principle. This would mean you need to break up your classes into more classes. You may also need to change some of the constructor parameters to be method parameters for the appropriate methods.
As others have mentioned, you could use traits. That would make it easier to inherit only the exact behavior you want. The traits can both create properties, set them using some default type, and provide a way to both get and set those properties.
As far as your concerns about things being able to change at runtime, I am not sure what you mean. It is all running at runtime. It is all just variables in the end. Constructors can be called at runtime, and you can pass whatever values they need. You can use setters at runtime as well. Dependency injection is by definition a runtime technique. If your class is creating the types in question without providing a way to change the type via DI, then I guess it is not changeable at runtime, but nobody is suggesting that.
You could utilize factory classes or static factory methods. Static factory methods can have different names that describe how it will be constructed. Factory classes can have a single factory method, in which case you would set your factory instances to whichever factory will construct the objects properly.
Don't forget about default arguments. These should be kept in mind no matter how you go about dependency injection or constructing your objects. Example:
class someClass {
public function _construct($serviceA = new serviceA()) {
$this->serviceA = $serviceA;
}
}
Upvotes: 0
Reputation: 3008
To keep things clean and maintainable for inheritance scenarios with a couple of dependencies, I personally favor setter injection over constructor injection. There are also some thoughts by Martin Fowler on this, which are worth a read.
You may also choose to inject the common dependencies with constructor injection and the child dependencies with setter injection to not mess up with the constructor.
Nevertheless, if you use setter injection it is very important to make sure an object is not partially initialized or missing some dependencies. A great way to ensure this is in the getter method of the service (albeit you'll only note this at runtime). Extending the example from Derek, you can define your getters as following:
trait ServiceATrait {
private $A;
public function initServiceA(ServiceA $serviceA) {
$this->A = $serviceA;
}
public function getServiceA() {
if (null === $this->A) {
throw new \LogicException("ServiceA has not been initialized in object of type " . __CLASS__);
}
return $this->A;
}
}
Whatever option you choose, make sure to use it consistently across your code base.
Upvotes: 0
Reputation: 4751
This seems like your best bet if you have to inject your dependency from whatever is creating your object:
class Child extends Base {
protected $C;
public function __construct(ServiceA $A, ServiceB $B, ChildDependency $C){
parent::__contruct($A, $B);
$this->C = $C;
}
}
You could try using Traits instead:
trait ServiceATrait {
public $A = new ServiceA();
public function getServiceA() { return $this->A; }
}
trait ServiceBTrait {
public $B = new ServiceB();
public function getServiceB() { return $this->B; }
}
class Base {
use ServiceATrait;
use ServiceBTrait;
}
class Child extends Base {
protected $C;
public function __construct(ChildDependency $C) {
$this->C = $C;
}
}
function() {
$c = new Child(new ChildDependency());
echo $c->getServiceB()->toString();
}
Upvotes: 4