Dependency Injection the best design pattern

4 mins
893

Deep understanding of the dependency injection design pattern the most important thing for an awesome developer.

Problem

<?php

namespace PluginName;

class Order {
	public $id;
}

class OrderProcessing {
	public function create_new_order() {
		/* Some kind of logic for create order */
		$this->log( 'Order created!' );
	}
	private function log( string $message ) {
		echo "Save log with message: {$message}" . PHP_EOL;
	}
}

$order_processing = new OrderProcessing();
$order_processing->create_new_order();

The first problem that you can see is the single responsibility principle. In OrderProcessing we are using the logger logic. Let’s move this logic to the single class.

<?php

namespace PluginName;

class Logger {
	public function log( string $message ) {
		echo "Save log with message: {$message}" . PHP_EOL;
	}
}

class OrderProcessing {
	public function create_new_order() {
		/* Some kind of logic for create order */
		$logger = new Logger();
		$logger->log('Order created!');
	}
}

$order_processing = new OrderProcessing();
$order_processing->create_new_order();

Small refactoring, and we can reuse the Logger class in the different parts of our application. But we can’t change the Logger storage so easily.

For understanding the dependencies of the OrderProcessing class needs to read the whole class. Each external function or class that is used inside our class are called dependencies. Some dependencies are good, for example, some PHP function which sanitizes some property. But in our example, we have a bad dependency new Logger that is called hard dependency.

Why is hard dependency bad? Because we can’t change logic without changing these classes. If I want to change the storage of log records, then I need to change both classes.

How do our example more flexible? Of course, use the dependency injection design pattern.

Introducing Dependency Injection

<?php

namespace PluginName;

class OrderProcessing {
	private $logger;
	public function __construct( Logger $loger ) {
		$this->logger = $logger;
	}
	public function create_new_order() {
		/* Some kind of logic for create order */
		$this->logger->log('Order created!');
	}
}

$logger           = new Logger();
$order_processing = new OrderProcessing( $logger );
$order_processing->create_new_order();

The best way it’s using the interface in the constructor instead of the Logger, but about it later.

More and more dependencies

<?php

namespace PluginName;

class OrderProcessing {
	private $logger;
	public function __construct( Logger $loger, OrderRepository $repository, SMSNotifier $sms_notifier ) {
		$this->logger       = $logger;
		$this->repository   = $repository;
 		$this->sms_notifier = $sms_notifier;
	}
	public function create_new_order() {
       		/* Some kind of logic for create order */
		$this->logger->log('Order created!');
	}
}

$repository       = new OrderRepository();
$sms_notifier     = new SMSNotifier();
$logger           = new Logger();
$order_processing = new OrderProcessing( $logger, $repository, $sms_notifier );
$order_processing->create_new_order();

The business logic grew, and the complexity of our constructor is getting higher. A lot of works need to do for adding a new feature. To manage all dependencies, we need to create the god object or move this logic to a different part. The best practice is to use the dependency injection container. Let’s try.

Dependency Injection Container

I hope you already know about autoloading your classes and files because this is the first thing to start with the dependency injection container.

There are many different packages for the dependency injection container. I prefer the package rdlowrey/auryn. Let’s installing it:

composer require rdlowrey/auryn

We’ve described that the OrderProcessing class has in the constructor arguments Logger, OrderRepository, and SMSNotifier.

But how to load the DIC?

<?php

use PluginName\OrderProcessing;
use Auryn\Injector;

require_once 'vendor/autoload.php';

$injector = new Injector();
$injector->make( OrderProcessing::class );

Instead of create new object as new OrderProcessing use $injector->make( OrderProcessing::class ).

Inversion Of Control (IoC)

Inversion of control (IoC) is a programming principle. IoC inverts the flow of control as compared to traditional control flow. In IoC, custom-written portions of a computer program receive the flow of control from a generic framework.

Wikipedia

This black magic helps you forget about new instances in your work. Just create a new file and describe the constructor with type hinting. Dependency injection container resolves all dependencies instead of when you try to get the object from the container.

Once you’ve added this wonderful tool, you simplify the interaction between elements, replacing it with type hinting.

How to manage the dependencies if we are using the interface?

Let’s create the interface ILogger:

<?php

namespace PluginName;

interface ILogger {

	public function message();

}

And rename Logger to the DBLogger:

<?php

namespace PluginName;

class DBLogger implements ILogger {

	public function message() {
		// TODO: Implement message() method.
	}

}

And change the construct for the OrderProcessing:

<?php

namespace PluginName;

class OrderProcessing {
	// ...
	public function __construct( ILogger $logger ) {
		// ...
	}
	// ...
}

And what happens? DIC can’t resolve your problems. Why? Because you need to exactly describe the class that you want to use in this class. So you need to resolve this problem manually:

<?php
// ...
$injector->make(
	OrderProcessing::class,
	[
		$injector->make( DBLogger::class ),
	]
);

This means we can automatically load all our dependencies but, in cases with abstractions, resolve it manually.

Conclusion

If you avoid the hard dependencies, this helps you create more quality and scalable solutions. To simplify your everyday development, use the simple and useful tool called a dependency injection container.

Leave a Reply

Your email address will not be published. Required fields are marked *