Dependency Injection, when virtual each class depends on several others

I am trying to implement best practices while learning PHP OOP. I understand the concept, but I doubt the correct implementation. Since I am trying to understand the basic implementation principle, I am not implementing the DI container in this piece of code.

Structure

  • Db class to connect to the database.

  • Settings , select options from db.

  • Languages , getting information for a specific language.

  • Page class, Product , class Customer and more.

Idea

Settings for the class requires a class Db .

Languages for finding information based on settings from the database require Db and Settings .

Page requires Db , Settings and Languages . Perhaps other classes may be needed in the future.

Simplified code

Db.php extends PDO

settings.php

class Settings { /* Database instance */ protected $db; /* Cached settings */ private $settings = array(); public function __construct(Db $db) { $this->db = $db; } public function load () { $selq = $this->db->query('SELECT setting, value FROM settings'); $this->settings = $selq->fetchAll(); } } 

Languages.php

 class Languages { public $language; protected $db; protected $settings; private $languages = array(); public function __construct(Db $db, Settings $settings) { $this->db = $db; $this->settings = $settings; // set value for $this->language based on user choice or default settings ... } public function load() { $this->languages = array(); $selq = $this->db->query('SELECT * FROM languages'); $this->languages = $selq->fetchAll(); } } 

page.php

 class Page { protected $db; protected $settings; protected $language; public function __construct(Db $db, Settings $settings, Languages $languages) { $this->db = $db; $this->settings = $settings; $this->languages = $languages; } public function load() { // load page info from db with certain settings and in proper language ... } } 

config.php

 $db = new Db; /* Load all settings */ $settings = new Settings($db); $settings->load(); /* Load all languages */ $languages = new Languages($db, $settings); $languages->load(); /* Instantiate page */ $page = new Page($db, $settings, $languages); 

I do not like the idea of ​​injecting the same classes over and over again. Thus, I will get to the point where I will need to introduce 10 classes. So my code is wrong from the start.

Perhaps the best way is to do the following:

config.php

 $db = new Db; /* Load all settings */ $settings = new Settings($db); $settings->load(); /* Load all languages */ $languages = new Languages($settings); $languages->load(); /* Instantiate page */ $page = new Page($languages); 

since the parameters already have access to $ db and $ languages ​​for the parameters $ db and $. However, this way I have to make calls like $ this-> languages-> settings-> db → ...

My whole code architecture seems completely wrong :) How should this be done?

+5
source share
3 answers

I will try to answer my own question, because I see this after studying a lot of materials.

1. The best practice is to create such objects as:

 $db = new Db(); $settings = new Settings ($db); $languages = new Languages ($db, $settings); // ... 

2. Use a DI container.

If you cannot write one, use the existing one. There are some that call themselves non-DI containers, such as Pimple (there are several posts on this site). Some of them are usually much slower and more complex (Zend, Symfony), then others, but also provide more functionality. If you are reading this, then you should probably choose a simpler one, such as Aura, Auryn, Dice, PHP-DI (in alphabetical order). It is also important to know that the corresponding DI containers (as I see) should be able to recursively cross dependencies, that is, find the dependencies needed for a particular object. They should also provide the ability to share the same object (for example, an instance of $ db).

3. When you try to create objects dynamically (if you use the front controller and routing), when you start dependencies manually, there will be many problems . That is why see paragraph 2.

See a great example here:

https://github.com/rdlowrey/Auryn#user-content-recursive-dependency-instantiation

https://github.com/rdlowrey/Auryn#instance-sharing

Video to watch:

https://www.youtube.com/watch?v=RlfLCWKxHJ0 (this is not PHP, but try to get an idea)

+3
source

There are some ideas to get rid of these ugly chain dependencies. Think of three specific classes A, B, C where A needs B and B needs C, so implicit A needs C. This is a very poor software design. Testing is more complicated since

  • If you need integration testing, you need these three objects.
  • if you want to test Isolated, then you need a layout for B and disable ctor, or you also need to mock C

At this point you should focus on your design. To separate A from C, you must create the interface on which BI A depends, and B implements.

From:

 A -> B B -> C 

now we have:

 A -> BI B impl BI B -> C 

We can completely separate them from another CI interface:

 A -> BI B impl BI B -> CI C impl CI 

Now our concrete class is untied, but if we connect them as we separated them (for example, through DI), there is no profit. So the next step is to compress the interfaces, for example, class A depends only on BI, which has only the methods that A needs.

Say this is done, and we need class A2, which needs some methods of B. You find that in fact BI would be good for A2, since BI has some methods of A2 (now we leave C and CI):

 A -> BI A2 -> BI B -> BI 

In a few days, you will find out that our BI interface is actually too large for A, and you want to reorganize BI so that as little BI as possible. But you cannot reorganize it, because A2 uses some methods that you want to remove.

It could be a bad design. Now we can do this (principle of interface separation):

 A -> BI A2 -> BI2 B impl BI B impl BI2 

In this case, we can now change the interfaces ourselves. No matter how many classes your project uses, I always recommend some type of creation template for part of your application to get (referring to the first examples) an object from an unknown implementation class that implements the CI interface.

Now this is the real OOP. Today you may have some kind of reworked chain of dependencies for getting CI, but overnight you dream of an innovative idea on how to get CI and change it the way you don’t need to change the code that uses CI! It is important.

I always say: it's all about hiding the implementation (in the right places, of course).

And now don’t start getting every class of the interface, but let some classes interact, and then reorganize them where you see the need.

For your classes, I think Settings is the destination monster / setter. It is not recommended to include classes for which only some settings are required, depending on the settings. This is better than I think:

 Settings -> DB Settings impl PageSettings Settings impl CustomerSettings Settings impl ProductSettings Page -> PageSettings Customer -> CustomerSettings Product -> ProductSettings 

I do not know how you use your language class, but I hope you now have an idea for software development. Of course there are more.

+1
source

If you use the dependency of one object inside another object, you create a dependency between these two components. This means that if you need to change the dependencies of your Settings class, it breaks everything that depends on your Settings class, which has it.

Dependency Injection Library

If you are worried about having to create your new objects manually each time, look at the dependency injection library to handle your object creation (like Pimple or AuraPHP DI ).

Using Pimple:

 // Define this once in your bootstrap use Pimple\Container; $container = new Container() $container['Db'] = function ($c) { return new Db(); }; $container['Settings'] = function ($c) { return new Settings($c['Db']); }; $container['Languages'] = function ($c) { return new Languages($c['Db'], $c['Settings']); }; $container['Page'] = function ($c) { return new Page($c['Db'], $c['Settings'], $c['Languages']); }; // Where ever you have access to your $container you can use this // (and it knows how to build your object for you every time). $page = $container['Page']; 

Interaction with suppliers

You can use accessor functions to set dependencies:

 class Settings { protected $db; function setDb(Db $db) { $this->db = $db; } // ... } $settings = new Settings(); $settings->setDb(new Db()); 

AuraPHP DI supports dependency injection with accessories.

 use Aura\Di\Container; use Aura\Di\Factory; $di = new Container(new Factory()); $di->set('db', new Db()); $di->set('settings', new Settings()); $di->setter['settings']['setDb'] = $di->get('db'); 

You can extend this even further and automatically introduce common dependencies that you do not want to manually install for each class (for example, PSR-3 Logger ) by entering based on the interfaces you extend.

 use Aura\Di\Container; use Aura\Di\Factory; use Psr\Log\LoggerAwareTrait; class Db { use LoggerAwareTrait; // ... } $di = new Container(new Factory()); $di->set('logger', new MyCustomLogger()); $di->set('db', new Db()); $di->setter['LoggerAwareTrait']['setLogger'] = $di->get('logger'); // $db->logger will contain an instance of MyCustomLogger // so will any other class that uses LoggerAwareTrait $db = $di->get('db'); 

Principle of shared responsibility

Of course, if your class has 10 different dependencies, then the problem may be with the design. The class must have sole responsibility .

Symptoms of a class that may violate the Single Responsibility Principle of action:

  • The class has many instance variables.
  • The class has many public methods.
  • Each class method uses different instance variables.
  • Specific tasks are delegated to a private method.

From " Packaging Design Principles " by Matthias Noback

0
source

Source: https://habr.com/ru/post/1212702/


All Articles