Service locator (SL) and dependency injection (DI) design patterns serve the same purpose. To make your code loosely coupled to have a better testable, maintainable, and extendable code. Both these patterns are implementing the inverse of control pattern. We use them to handle the dependencies between multiple classes.
Service locator and dependency injection container (DiC) are trying to handle dependencies of your object in one place. The difference is that with SL you are defining the classes (services) in one place and you are letting the classes take care of their dependencies. Whereas with DiC you are providing the dependencies to the classes.
Tightly/loosely coupled code
We’ll explain the code coupling in a simple example. Say we have a class that needs to interact with some kind of a database (the most usual example you will find out in the wild). We have a Redis database and the code looks like this (simplified)
<?php
class RedisDb {
public function get(string $key) : string {/* ... */}
}
Code language: HTML, XML (xml)
.Redis is not a traditional a database like PostgreSQL or MySQL, but it can be used for persisting some data in-memory.
The class that needs to interact with this could then look like this
<?php
class ExampleService {
public function executeCode() {
$redisInstance = new RedisDb();
$data = $redisInstance->get('someDataKey');
// Business logic goes here.
}
}
Code language: HTML, XML (xml)
While the above is a valid code, it’s tightly coupled. That means that the ExampleService
class has a direct dependency on the RedisDb
class. Because it instantiated it in the public method. If you make some changes to the RedisDb
class, you’ll need to make sure you didn’t make any breaking change in it. That would directly impact the ExampleService
class. Extending it is also hard, because you need to take care of the added dependency (RedisDb
). Plus, such classes are really hard to test. We’ll either need to provide the RedisDb
class in the tests, or mock it and override the RedisDb
in the tests with our mock. Which is often times very difficult and time consuming.
One way to get rid of this tight coupling is to inject our RedisDb
into the ExampleService
class using constructor injection.
<?php
class ExampleService {
private $redisInstance;
public function __construct(RedisDb $redisInstance) {
$this->redisInstance = $redisInstance;
}
public function executeCode() {
$data = $this->redisInstance->get('someDataKey');
// Business logic goes here.
}
}
Code language: HTML, XML (xml)
This way we are passing the RedisDb
as a dependency into our ExampleService
class, making it less coupled. This is ok, but we can do better.
Dependency inversion
One of the principles of OOP is the dependency inversion principle (one of the SOLID principles). It’s often described as: depend on abstractions, not concrete implementations. For that we need interfaces. Interfaces are contracts – they are only describing what public methods our classes need to implement. They won’t contain any business logic. That is left for the class that implements the interface to do (to honor the contract set by the interface). We also mentioned public methods, because those are the methods exposed to the world (our app).
In our example we would create a Database
interface
<?php
interface Database {
public function get(string $key) : string;
}
Code language: HTML, XML (xml)
Now we can implement this for many different databases: MySQL, PostgreSQL, Redis etc.
<?php
class RedisDb implements Database {
public function get(string $key): string {/* concrete implementation */}
}
Code language: HTML, XML (xml)
<?php
class ExampleService {
private $database;
public function __construct(Database $database) {
$this->database = $database;
}
public function executeCode() {
$data = $this->database->get('someDataKey');
// Business logic goes here.
}
}
Code language: HTML, XML (xml)
Notice how we used Database
as an injector type hint in the ExampleService
constructor. We no longer depend on the concrete implementation. We can easily replace this with a different database at any point in time. This is the example of loosely coupled code. This is something we can easily test, as the provided instance can be a mock implementing the Database
interface.
Service locator drawback
Service locator is basically a container containing all the dependencies in one place. We can then inject this container in our service and use whatever dependency we need from it. An example would be
<?php
class ExampleService {
private $container;
public function __construct(ContainerInterface $container) {
$this->container = $container;
}
public function executeCode() {
$data = $this->container->useDb('database')->get('someDataKey');
}
}
Code language: HTML, XML (xml)
While this sounds fine, it’s problematic because we don’t know what is the actual dependency of our class.
We only know that our Service class depends on the ContainerInterface, but apart from that, we don’t know precisely which one it is. We only know that it needs to work with a database in the executeCode
method. In contrast to this, when we used dependency injection in the ExampleService
, we knew exactly that it depends on the Database
interface (it expects the database).
While in a sense you are decoupling your code like with dependency injection, you are obfuscating the dependency graph. This can make it harder for you to debug errors in case anything breaks when the code changes, and it may make the system difficult to maintain.
Dependency injection container
In our WordPress projects we recently started implementing the dependency injection container using the php-di library. This library is great because it offers automatic autowiring of the dependencies. Autowiring is the ability of the container to automatically create and inject dependencies.
WordPress is a bit specific because it works with hooks (actions and filters, not React hooks). So to initialize the class we need to register our actions and filters that are located in the class. For this we use a register()
method as a place for the hooks. At our plugin initial point we add a list of classes in the get_service_classes()
method, which returns an array of fully qualified class names
<?php
private function get_service_classes() : array {
return [
Admin\Optimizations::class,
Users\User_Manager::class,
...
];
}
Code language: HTML, XML (xml)
Then we initialize the DI container and kickstart our WordPress classes like this
<?php
use \DI\ContainerBuilder;
...
final class Plugin implements Registerable, Has_Activation, Has_Deactivation {
private $services = [];
public function register_services() {
// Bail early so we don't instantiate services twice.
if ( ! empty( $this->services ) ) {
return;
}
$builder = new ContainerBuilder();
$container = $builder->build();
$this->services = array_map(
function( $class ) use ( $container ) {
return $container->get( $class );
},
$this->get_service_classes()
);
array_walk(
$this->services,
function( $class ) {
if ( ! $class instanceof Service ) {
throw Exception\Invalid_Service::from_service( $class );
}
$class->register();
}
);
}
}
Code language: PHP (php)
This way we first autowired our classes, and then we pull the classes with the register()
method and then we invoke this method. The register_services()
method hooks to plugins_loaded
action in our plugin, and this way all our hooks are properly invoked when needed.
Conclusion
Working with real life applications means that your code will get complicated. Parts of the code will need to interact one with another, perfect separation is an ideal that we can strive to, but will most probably never achieve. That is why we are dependent on things like dependency injection (no pun intended).
While DI can be a bit complicated to learn at first, it’s benefits are numerous. It will help you with the unit testing your code. Reduce the boilerplate code. Extending the application becomes easier (like in the database example) and it loosens the code coupling. So go ahead and give it a try if you are not using it already.
Leave a Reply