Containerless Dependency Injection for Services
While I’m a big fan of dependency injection, I find dependency injection containers less alluring. I’ve found that liberal usage of containers can make application code harder to follow, and often the container definitions go untested, creating a spawning pool for bugs. I’ve seen a number of frameworks/applications which effectively treat the container as a very fancy global variable that classes inevitably pull objects out of. These issues are a symptom of the tool being used incorrectly, but containers encourage this behaviour because they make it so easy. Containers sometimes encourage ‘shared’ services, which are effectively global variables. Putting this shared mutable state in a container does not fix the problems related to global state, but it lets developers pretend they’re not exposed to the risks globals present. Every application I have worked on has some shared global state. Database connections and environment variables are two examples that come to mind. Using singletons and global functions continuously reminds you that you’re playing with fire. Whereas containers make you feel safe. In smaller or simpler applications, I think that the complexity incurred by a dependency injection container is not offset by its benefits. In these situations I’ve found myself using a simpler solution that I’d like to share.
Service Dependencies
When building applications, I like to use ‘service’ classes. These classes are generally stateless objects that present top level domain actions in the application. For example, the UsersService
might expose a ensureUserExists
or getUser
method. The service methods encapsulate the various interations that are related to these domain actions. A register
method might do the following:
- Update the
users
andprofiles
database table. - Track some metrics in statsd.
- Fire off a verification email.
An example service in an application I’ve been working on looks a little bit like this:
- <?php
- namespace App\Service;
- class UsersService
- {
- use \Cake\Core\InstanceConfigTrait;
- use \Cake\Datasource\LocatorAwareTrait;
- use \Cake\Datasource\ModelAwareTrait;
- private $http, $stats, $Users;
- public function __construct($http, $stats, $usersTable = null)
- {
- $this->http = $http;
- $this->stats = $stats;
- $this->Users = $usersTable;
- if ($this->Users === null) {
- $this->modelFactory('Table', [$this->tableLocator(), 'get']);
- $this->loadModel('Users');
- }
- }
- /**
- * Get the user data that relates to an access token.
- *
- * Having the user data lets us persist it to the database, and do useful work
- * later.
- */
- public function getUser($accessToken)
- {
- $res = $this->http->get('/user', ['access_token' => $accessToken]);
- $this->stats->increment('user.fetched');
- return $res->json;
- }
- /**
- * Get the user and ensure it exists in the database.
- */
- public function ensureUser($accessToken)
- {
- $this->stats->increment('user.ensured');
- $data = $this->getUser($accessToken);
- return $this->Users->ensureExists($data, $accessToken);
- }
- }
This service interacts with both the Github API, statsd and the UsersTable. All of its dependencies can be injected through the constructor, and have sensible defaults where possible. This allows me to easily leverage mocks in my tests, and not lean on global state inside the service.
Getting Dependencies
My service has a few dependencies, and I’d prefer to not duplicate code when constructing instances. Because I don’t want the complexity that a container adds, and don’t want to have shared state, or inheritance I can use a trait. Traits allow horizontal code reuse and fit my requirements well:
- Traits are simple. My editor can easily jump to a method definition.
- No code duplication. I can re-use the trait in both HTTP and CLI contexts.
- No shared state. The trait methods can return new instances each time they are called.
In this application my ServiceTrait
looks something like the following:
- namespace App\Controllers;
- use Cake\Core\Configure;
- use Cake\Network\Http\Client as HttpClient;
- use App\Services\UsersService;
- use League\StatsD\Client as StatsClient;
- trait ServicesTrait {
- protected function stats()
- {
- $client = new StatsClient();
- $client->configure(Configure::read('Statsd'));
- return $client;
- }
- protected function githubClient()
- {
- $config = Configure::read('Github');
- return new HttpClient([
- 'host' => $config['apiHost'],
- 'scheme' => 'https',
- 'redirect' => 3
- ]);
- }
- protected function userService()
- {
- return new UsersService($this->githubClient(), $this->stats());
- }
- // More services
- }
We can use this trait in both our HTTP controllers, and our CLI tools. The code is easy to follow, and an IDE or editor can easily resolve what $this->stats()
does in any context. I avoid the complexity and ambiguity that containers can introduce. Furthemore, my gloal state is explictly contained in Configure
. In tests, I can also easily mock out a service, by stubbing one of the service factory methods. I find this solution is a simple way to get dependency injection without a container, hopefully you can use it in one of your projects.
Hi! This approach looks interesting.
What do you do if you need to return in one of your service bitbucketClient with the same interface like githubClient? Will you make a new method “bitbucketClient” in your trait? Or let’s say githubClient but with another parameter- will you make a new method “githubClientWithHTTPParam”?
How do you solve these type of issues?
swegey on 2/2/16
@swegey I would add a new method in the trait and have the two services share a common interface.
mark story on 2/4/16
How can we test class with this aproach? How can we remplace realisation in ServicesTrait?
Londeren on 2/15/16
Thanks so much!
uçak bileti on 2/24/16
saraybosna turu on 2/24/16
Londeren: Generally I only have to replace the implementations in test cases. In those situations I can stub out the factory method and have the mocked method return an alternate implementation.
Saraybosna turu: I would probably define a new factory method, or change the original method definition. In an application with a container, you’d either need to add a new service, or mutate a shared service. I think mutating a shared service is not a good idea, and would prefer to add more services. In this approach service names translate into factory methods.
mark story on 3/6/16
Thank you very much. i had worked with laravel. I saw this facilities i didn’t think it is in cakephp 3.x .
mehdi fathi on 10/24/16