Alex Lomakin
Alex Lomakin

Reputation: 433

Dependancy Injection where virtualy every class is dependant on several others

I'm trying to implement best practices while learning PHP OOP. I understand the concept, but kind of doubt with proper implementation. As I'm trying to figure out the basic implementation principle, I'm not implementing DI container in this piece of code.

Structure

Idea

Settings class needs Db class to retrieve settings.

Languages class needs both Db and Settings to retreive information based on settings from a database.

Page class needs Db, Settings and Languages. It may also need some other classes 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 don't like the idea of injection same classes over and over again. This way, I'll get to the point, where I'll need to inject 10 classes. So, my code is wrong from the beginning.

Maybe, a better 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);

as Settings already have access to $db, and $languages to both $db and $settings. However in this manner, I'll have to make calls like $this->languages->settings->db->...

All my code architecture seems to be completely wrong :) How it should be done?

Upvotes: 5

Views: 1300

Answers (3)

Aitch
Aitch

Reputation: 1697

There are some ideas to get rid of those ugly chain dependencies. Think of three concrete class A, B, C, where A needs B, and B needs C, so implicit A needs C. This is very bad software design. Testing is more complicated since

  1. if want some integration testing you need those three objects
  2. if you want to test A isolated, then you need a mock for B and disable the ctor or also have to mock C

At this point you should stop with your design. To decouple A from C you should create an interface BI A depends on and B implements.

From:

A -> B
B -> C

we now have:

A -> BI
B impl BI
B -> C

We can completely decouple them with another interface CI:

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

Now our concrete class are decoupled, but if we connect them as we have separated them (for example via DI) there is no profit. So the next step is to shrink the interfaces to have e.g. a class A to only depend on a BI which only has the methods which A needs.

Let's say this is done and we need a class A2 which needs some methods of B. You find out that actually BI would be nice for A2, since BI has some methods A2 needs (we now leave C and CI away):

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

Some days later you find out that our interface BI is actually too big for A and you want to refactor BI to keep BI as small as possible. But you cannot refactor it, because A2 uses some methods you want to drop.

This might also be bad design. What we can now do is (interface segregation principle):

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

In this case we can now change the interfaces independently. No matter how many classes your project use I always recommend some type of creational pattern for parts of your application to get (refering to the first examples) an object from an unknown implementation class implementing a CI interface.

That is now real OOP. Today you might have some over-engineered dependency chain of getting a CI, but overnight you dream about a groundbreaking idea how to get a CI and change it in a way you NOT need to change any code that uses a CI! This is important.

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

And now don't start to get every class an interface, but let the coupling of some classes happen and then refactor them where you see the need.

Applied to your classes I think Settings is a getter/setter monster. It's not a good idea to let the classes, which only need some settings, to depend on Settings. It's better that way I think:

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

I don't know how you use your Language class but I hope you now have an idea of designing software. Of course there's more.

Upvotes: 1

Alex Lomakin
Alex Lomakin

Reputation: 433

I'll try to answer my own question, as I see it after studying lots and lots of materials.

1. Best practice is to create objects like:

$db = new Db();

$settings = new Settings ($db);

$languages = new Languages ($db, $settings);

// ...

2. Use DI container.

If you can't write one, use existing one. There are some, which call themselves DI containers not being them, like Pimple (there are several post about that on this site). Some tend to be much slower and much more complicated (Zend, Symfony), then the others, but also provide greater functionality. If you are reading this, then you should probably choose more simple one, like Aura, Auryn, Dice, PHP-DI (in alphabetical order). It's also important to know, that proper DI containers (as I see it) should have the ability to recursively traverse dependencies, meaning find dependencies needed for a certain object. They should also provide an ability to share same object (like $db instance).

3. Injecting dependencies manually will cause you lots of problems when trying to create objects dynamically (in case you use front controller and routing). That's why see point 2.

See 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 (it's not PHP, but try to get the idea)

Upvotes: 5

xsist10
xsist10

Reputation: 3051

If you use the dependency of one object inside another object, then you're creating a dependency between those 2 components. It means that if you need to change the dependencies of your Settings class, it breaks everything that relies on your Settings class having it.

Dependency Injection Library

If you're worried about having to build your new objects manually each time, have a look at a 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'];

Injecting dependencies with Accessors

You can use accessors 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 using accessors.

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 inject common dependencies that you don't want to manually set on each class (like a PSR-3 Logger) by injecting 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');

Single Responsibility Principle

Of course, if your class has 10 different dependencies, then the problem might be with the design. A class should have a single responsibility.

Symptoms of a class that may violate the Single responsibility principle:

  • The class has many instance variables
  • The class has many public methods
  • Each method of the class uses other instance variables
  • Specific tasks are delegated to private method

From "Principles of Package Design" by Matthias Noback

Upvotes: 0

Related Questions