MooseX::DIC - A dependency injector container for Moose
Full documentation on the MooseX::DIC Webpage.
MooseX::DIC is a dependency injection container tailored to Moose, living in a full OOP environment and greatly inspired by Java DIC frameworks like Spring or CDI.
The goal of this library is to provide an easy to use DI container without configuration files and with automatic wiring of dependencies via constructor by class type (ideally by Role/Interface).
The configuration is performed by the use of Marker roles and a specific trait on attributes that have to be injected.
One of the principal tenets of the library is that while code may be poluted by the use of DIC roles and traits, it should work without a running container. The classes are fully functional without the dependency injection, the library is just a convenient way to wire dependencies (this is mainly accomplished by forbidding non constructor injection).
This library is designed to be used on long-running processes where startup time is not a concern (within reason, of course). The container will scan all configured paths to look for services to inject and classes that need injection.
There is a great amount of flexibility to account for testing environments, non-moose libraries, alternative implementations of services, etc, although none of it is needed for a simple usage.
A service is injectable if it consumes the Role MooseX::DIC::Injectable, which is a parameterized role.
package MyApp::LDAPAuthService;
use Moose;
with 'MyApp::AuthService';
with 'MooseX::DIC::Injectable' => {
implements => 'MyApp::AuthService',
qualifiers => [ 'LDAP' ],
environment => 'test',
scope => 'singleton'
};
has ldap => (
is => 'ro',
does => 'LDAP',
traits => ['Injected']
);
1;
We can see that this service is both an injectable service and consumes another injectable service,LDAP. We register a class as injectable into the container registry by consuming the MooseX::DIC::Injectable role, and we get injected dependencies by using the Injected trait.
None of the parameters of the MooseX::DIC::Injectable role are mandatory, they have defaults or can be inferred.
On the example above, the role/interface the LDAPAuthService was implementing could be inferred from the
with 'MyApp::AuthService'
previous line.
To use this service:
package MyApp::LoginController;
use Moose;
use Moosex::DIC;
has auth_service => ( is=>'ro', does => 'MyApp::AuthService', injected );
sub do_login {
my ($self,$request) = @_;
if($self->auth_service->login($request->username,$request->password)) {
print 'this is fine';
}
}
1;
Here we made use of the exported injected
function from the MooseX::DIC package to define the traits, a little
syntactic sugar if you only use the Injected trait.
When starting your application, the container must be launched to start it's scanning. You can tell the container which folders to scan in search of injectable services. This operation is slow as it has to scan every file under the specified folders, which means you will usually only use one container per application.
To start the container:
#!/usr/bin/env perl
use strict;
use warning;
use MooseX::DIC 'build_container';
use MyApp::Launcher;
# This may take some time depending on your lib size
my $container = build_container( scan_path => [ 'lib' ] );
# The launcher is a fully injected service, with all dependencies
# provided by the container.
my $app = $container->get_service 'MyApp::Launcher';
$app->start;
exit 0;
Although the vast majority of services we want to inject are by their stateless nature candidates to be singletons, we may want for our service to be instantiated every time they are requested. For example, an http agent could be instantiated once per service.
package MyApp::LWPHTTPAgent;
use LWP::UserAgent;
use Moose;
with 'MyApp::HTTPAgent';
with 'MooseX::DIC::Injectable' => { scope => 'request' };
has ua => ( is => 'ro', isa => 'LWP::UserAgent', default => sub { LWP::UserAgent->new; } );
sub request {
$self->ua->request(shift);
}
1;
This service declares that it can be injected on attributes that need an object that does 'MyApp::HTTPAgent' and that each time it is called, it will be created anew. To use it:
package MyApp::RESTUserService;
use Moose;
with 'MyApp::UserService';
has http_client => ( is => 'ro', does => 'MyApp::HTTPAgent', traits => [ 'Inject' ] );
sub persist {
my ($self,$user) = @_;
# A new instance is created here and lives for as long as
# the RESTUserService lives.
$self->http_client->request(...);
}
Two types of scope are available for services:
-
singleton
The default scope, the registry will only keep one copy of the service and will inject it into every attribute it is requested.
Make sure the service is stateless or you will run into race conditions.
-
request
Each time the service is requested, a new instance of it will be created. Useful for stateful services.
For services which are request scoped, the requester can also ask the injection container to create a new service each time the accessor is used, for stateful services that should only live once per use. For example, we may be interested in using an http user agent that somehow keeps some states between callings and if used for different purposes would be corrupted.
package MyApp::RESTUserService;
use Moose;
with 'MyApp::UserService';
has http_client => ( is => 'ro', does => 'MyApp::HTTPAgent', scope => 'request', traits => [ 'Inject' ] );
sub persist {
my ($self,$user) = @_;
# A new instance of MyApp::LWPHTTPAgent is created here
$self->http_client->request(...);
# Yet another instance of MyApp::LWPHTTPAgent is created again
$self->http_client->request(...);
# If we want to keep the same instance for a series of calls, reference it.
my $ua = $self->http_client;
$ua->request(...);
$ua->request(...);
}
There are two scopes available for the injection scope:
-
object
The default scope. For request scoped services, the service is instantiated once per object.
-
request
For request scoped services, if the injection scope is request too, an accessor is created that will fetch a new instance of the service each time it is called.
The injection scope only makes sense for request scoped services, since singleton services will only be instantiated once.
It is a configuration error to ask for a singleton scoped service into a request-scoped injection point, and the container will generate an exception when it encounters this situation (in the spirit of detecting errors as soon as possible).
Sometimes, we want a Role/Interface to be implemented by many classes and to let the caller specify which one it wants.
While this would seem to oppose the very idea of letting a container to give you objects, in fact it doesn't, and gives a great deal of flexibility while still allowing the container to choose the best implementator for your caller and initialize it.
Qualifiers let a service specify with a more fine-grained precision how they implement an interface, so that callers can choose them based on those qualifiers.
For example, we can have two implementators of an HTTPAgent service:
package MyApp::LWPHTTPAgent;
use Moose;
with 'MyApp::HTTPAgent';
with 'MooseX::DIC::Injectable' => { qualifiers => [ 'sync' ] };
sub request {
# returns the response
}
package MyApp::AsyncHTTPAgent;
use Moose;
with 'MyApp::HTTPAgent';
with 'MooseX::DIC::Injectable' => { qualifiers => [ 'async' ] };
sub request {
# returns a Promise with the response
}
package MyApp::RESTUserService;
use Moose;
use MooseX::DIC;
has http_client => ( is => 'ro', does => 'MyApp::HTTPAgent', qualifiers => [ 'async' ], inject);
sub persist {
# This service knows it can expect a Promise result
# from the http agent, since it asked for the async version.
return $self->http_client->request(...)
->then(sub {
...
})
->catch(sub {
...
});
}
It is a configuration error to have two implementators of the same service living in the same environment without at least one of them having a qualifier, and the container will generate an exception when it encounters that situation.
When there are competing implementators for the same caller, which have different qualifiers, the resolution is based on the following rule: The longest most precise qualifier match is returned
If the caller requests for qualifiers 'a','b' and 'c', given the following service implementations:
- Impl1 => qualifiers 'a','d'
- Impl2 => qualifiers 'b', 'c'
- Impl3 => qualifiers 'a'
The implementator Impl2 will be selected, since it has the greater number of matching qualifiers.
If no exact qualifier match is found, the next best match is selected. Example:
Given a caller that requests a Service with qualifiers 'a', 'b', and 'c'. For the following implementations:
- Impl1 => qualifiers 'a'
- Impl2 => no qualifiers
The Impl1 will be selected even though it doesn't match all caller qualifiers.
Given a caller that requests a Service with qualifiers and only one implementator with no qualifiers, the implementator will still be selected.
Given a caller that requests a Service with qualifier 'a', for the following implementations:
- Impl1 => qualifier 'b'
- Impl2 => qualifier 'c'
- Impl3 => no qualifiers
One of the three implementations (always randomly) will be returned, since they are all equal matches. The random selection will be enforced to avoid library clients shooting themselves on the foot by relying on a specific selection when there are equal matches.
Following the last example, if a client specifically wants an implementation with no qualifiers it can specify it by setting the qualifier parameter of the attribute to empty array:
package MyApp::ExampleController;
use Moose;
use MooseX::DIC;
has service => ( is => 'ro', does => 'ServiceRole', qualifiers => [], inject );
Sometimes, we want the wiring of services to depend on a runtime environment. To this end, we use the concept of environments.
By default (that is, if no environment is declared by an Injectable
service) all services live inside the 'default'
environment. But we can do more. Let's consider the following services:
package MyApp::UserRepository;
use Moose::Role;
package MyApp::UserRepository::Database;
use Moose;
with 'MyApp::UserRepository';
with 'MooseX::DIC::Injectable' => { environment => 'production' };
package MyApp::UserRepository::InMemory;
use Moose;
with 'MyApp::UserRepository';
with 'MooseX::DIC::Injectable' => { environment => 'test' };
With the following caller:
package MyApp::UserController;
use Moose;
use MooseX::DIC;
has repository => (is => 'ro', does => 'MyApp::UserRepository', inject );
sub do_something {
my ($self,$user) = @_;
$self->repository->persist($user);
}
These implementations live in different environments and they won't see each other. The selection of one or the other will depend on which environment we launch the container in, as in:
#!/usr/bin/env perl
use strict;
use warning;
use MooseX::DIC::Container;
use MyApp::UserController;
my $container = MooseX::DIC::Container->new( environment => 'test' );
# In the test environment, the UserController class will have received
# The InMemory user repository.
my $user_controller = $container->get_service 'MyApp::UserController'
When the container doesn't find a service in a given environment, it will fall back to the default environment. If it doesn't find a service there, it will throw an exception.
Loïc Prieto Dehennault
CPAN ID: LPRIETO
CAPSiDE
[email protected]
https://metacpan.org/pod/Moose
http://docs.oracle.com/javaee/6/tutorial/doc/giwhl.html
The source code is located here: https://github.com/loic-prieto/moosex-dic
Please report bugs to: https://github.com/loic-prieto/moosex-dic/issues
Copyright (c) 2017 by CAPSiDE
This code is distributed under the Apache 2 License. The full text of the license can be found in the LICENSE file included with this module.