2. Registry
2.1. Overview
The registry is at the heart of any dependency-injected application or library. All services are registered with the registry, so that when an application needs an instance of a particular service, it may obtain that service reference by querying the registry.
In order to use Needle, you only really need to understand how to create and manipulate registry objects.
2.2. Creating
Creating a registry is as simple as calling Needle::Registry.new
. This will give you a new registry object, bootstrapped to contain a few general services.
1 2 3 | require 'needle' registry = Needle::Registry.new |
Once you have the reference to the registry, you can register services with it, create new namespaces in it, and so forth.
Alternatively, you can pass a block to #new
:
1 2 3 | registry = Needle::Registry.new do |r| ... end |
The parameter to the block will be a reference to the registry. This allows you to register services with the registry as soon as it is created.
Another convenience method is #define!
:
1 2 3 | registry = Needle::Registry.define! do ... end |
This block accepts no parameters, and evaluates the block as if it were passed to Registry#define!
(see below).
There can be problems with using define!
, however, since it uses instance_eval
to evaluate the block within the context of another object. If you find yourself running into scoping issues, you might want to consider using #define
:
1 2 3 | registry = Needle::Registry.define do |b| ... end |
This block accepts a single parameter—a “builder” object to aid in registering services—and evaluates the block as if it were passed to Registry#define
(see below).
2.3. Services
Registering services with a Needle registry is very straightforward. The simplest way to do it is:
registry.register( :foo ) { Bar.new }
The above will register a new service with the registry, naming it :foo
. When :foo
is requested from the registry, a new instance of Bar
will be instantiated and returned.
You get services from the registry in either of two ways:
1 2 3 4 5 | # Treating the registry as a Hash svc = registry[:foo] # Treating the service as a property of the registry svc = registry.foo |
Convenience Methods
Because you will often need to register many services with a registry at once, two convenience methods have been provided to make this use case lean and mean.
The first is define
. Just pass a block to define that accepts one parameter. This parameter will be a “builder” object that allows you to define services just by sending them as messages to the builder:
1 2 3 4 5 | registry.define do |b| b.foo { Bar.new } b.bar { Foo.new } ... end |
Alternative, you can call define!
, passing a block that accepts no parameters. This block will be evaluated in the “builder” object’s context, with any unrecognized method call being interpreted as a new service registration of that name:
1 2 3 4 5 | registry.define! do foo { Bar.new } bar { Foo.new } ... end |
Both of the above will register two new services with the registry, :foo
and :bar
.
Default Lifestyle
By default, a service is only instantiated once per registry. This means that (using the above example) if the service :foo
were queried twice, the registry would return the same object for both queries:
1 2 3 4 | svc1 = registry.foo svc2 = registry.foo p svc1.object_id == svc2.object_id #=> true |
You can change this behavior, with service models. See the chapter on Service Models for more information.
Parameterized Services
Needle also supports parameterized services. These are services that, when requested, require contextual information to be passed as a parameter so that the service can be correctly initialized.
Consider the following example, in which some hypothetical Printer
class represents any of a number of printers, based on a parameter given to its constructor.
1 2 3 4 5 6 | registry.register( :printer, :model => :multiton ) do |c,p,name| Printer.new( c.log_for( p ), name ) end mono = registry.printer( :monochrome ) color = registry.printer( :color ) |
There are a few things to note about the above example:
- The
:multiton
model is explicitly requested. This is necessary because the default service model (:singleton
) does not allow parameterized services. Most of the time, you’ll use the multiton service model with parameterized services, but you don’t have to. You could also use any of the prototype models as well.
- The constructor block for the
:printer
service takes three parameters,c
,p
, andname
. The first two parameters,c
andp
, represent the container and the service point, respectively. Any parameters after those two are the contextual parameters given when the service is requested. In this case, there is only one contextual parameter: the name of the printer.
- Notice the first parameter to the Printer constructor:
c.log_for(p)
. This is itself invoking a parameterized service, named:log_for
, and passingp
as the contextual information. This will return a new logger handle for the service pointp
(i.e., the current service point).
- See how the printer service is requested on the last two lines. In this case, the
#printer
message is sent to the registry with a single parameter. You can also request the service in two other ways:
1 2 | dot_matrix = registry[ :printer, :dot_matrix ] ink_jet = registry.get( :printer, :ink_jet ) |
Choose the style that works best for you.
2.4. Namespaces
Namespaces allow you to organize your services. The feature has many different applications, including:
- Third-parties may distribute Needle-enabled libraries without worrying about their choice of service names conflicting with the service names of their clients.
- Developers may organize complex applications into modules, and the service definitions may be stored in the registry to reflect that organization.
- Services deeper in the hierarchy may override services higher up.
Creating a namespace is as easy as invoking the #namespace
method of the registry (or of another namespace):
registry.namespace :stuff
This would create a new namespace in the registry called :stuff
. The application may then proceed to register services inside that namespace:
1 2 3 | registry.stuff.register( :foo ) { Bar.new } ... svc = registry.stuff.foo |
Here’s a tip: namespaces are just a special kind of service. This means that you can access namespaces in the same ways that you can access services:
svc = registry[:stuff][:foo]
Convenience Methods
Because it is often the case that you will be creating a namespace and then immediately registering services on it, you can pass a block to namespace
. The block will receive a reference to the new namespace:
1 2 3 4 | registry.namespace :stuff do |spc| spc.register( :foo ) { Bar.new } ... end |
If you prefer the define
approach to registering services, you may like namespace_define
, which creates the new namespace and immediately calls define
on it:
1 2 3 4 | registry.namespace_define :stuff do |b| b.foo { Bar.new } ... end |
And, to mirror the namespace_define
method, there is also a namespace_define!
method. This method creates a new namespace and then does a define!
call on that namespace.
1 2 3 4 | registry.namespace_define! :stuff do foo { Bar.new } ... end |
The above code would create a new namespace called :stuff
in the registry, and would then proceed to register a service called :foo
in the new namespace.