7. Service Models
Service models are used to specify when a service point should be instantiated. There are five pre-defined service models, and you can easily define more yourself should you need them.
The service model for a service point is specified using the model
attribute in the service point definition. This attribute is optional, defaulting to singleton-deferred
, but if given it must refer to either one of the five standard service models, or to a custom service model you have defined.
7.1. Standard Models
The five standard service models are:
prototype |
A service point using this model will be reinstantiated every time it is requested. This may be useful, for instance, when the requested service is stateful, and you do not want instances of the service to be reused. |
prototype-deferred |
This is the same as the prototype service model, except that the service will not be instantiated until the first time any of its methods are invoked. Thus, if you might need the service, but there are some paths through your program that won’t use it, you can use this model so that the overhead of initializing the service only occurs when you actually invoke a method on it. |
singleton |
Only one instance of the service is ever created. Any subsequent request for the service will return the instance that was created the first time. This is the most common scenario for most services, since in general a service is either stateless, or its state is effectively global throughout an application. |
singleton-deferred |
This is identical to the singleton model, above, but also has the deferring functionality of the prototype-deferred model. |
threaded |
This model results in every thread obtaining its own instance of a service. It is otherwise identical to singleton-deferred . |
7.2. Deferred Instantiation
Using one of the “deferred” service models (prototype-deferred
, singleton-deferred
, or threaded
) can be very useful, but there are side-effects that you ought to be aware of. Otherwise, you may spend a lot of time debugging some difficult-to-track-down problems.
In particular, it is important to keep in mind that the service is not actually instantiated until a method is invoked on it. This means that if the service’s initialization routine raises an error, the exception will not occur where the reference to the service was obtained, but will occur where the method call was made. The service will also not have been properly instantiated, so if you try to inoke any of its methods again, you’ll get nil
back every time.
For example, consider the following scenario.
Suppose you are writing a web-application framework, using Copland to modularize the various components. One of the services you create is a “page manager”, which will broker all requests for page objects. When an application needs a page, it queries the page manager, which will look up the page and return it.
You make the page manager service singleton-deferred
.
Now, inside the controller for your architecture, you grab a reference to the page manager. (Note, however, that the object you are given is not the page manager—instead, it is a proxy object that will instantiate the page manager the first time a method is invoked on it.) Some time later you invoke #get_page
on the page manager, which you expect to go grab a page object and return it.
However. What actually happens is this: the proxy object intercepts the method call and tries to instantiate the page manager service, since it hasn’t been instantiated yet. During the page manager initialization, however, something goes wrong. Perhaps one of the page classes doesn’t exist, or there is a syntax error in one of the files that the page manager needs to require. Whatever it is, the page manager cannot be initialized and an exception is thrown.
The controller code catches the exception.
Problem. Inside the rescue clause for the exception, the controller queries the page manager for the error page, so that it can display a coherent error to the client. However, since there was an error instantiating the page manager, future queries to the proxy object always return nil
. This may result in an “undefined method `x’ for nil:NilClass” error down the road.
To avoid problems like this, you can do one of two things. One, you can use the singleton
model, instead of singleton-deferred
. This way, Copland will never deliver you a reference to a proxy. The other solution is to create a method in your class that always returns non-nil
. Then, if it ever does return nil
, you can be pretty sure that the service was not instantiated correctly. This, unfortunately, requires your code to know that the service was deferred.
7.3. Custom Service Models
Creating your own service models is very straight-forward. In general, all you need to do is create a class. The class should extend the Copland::Instantiator::Abstract
class, and must override the instantiate
method. The instantiate
method should accept a no parameters. The method should then return an object that represents the instantiated service.
After creating the class, you need to register it with the Copland::ClassFactory
class by calling it’s register
method, passing the name to want for this new model, and a reference to the class that implements it.
To return a deferred version of the service, return a new instance of Copland::Instantiator::Proxy
. The proxy’s constructor accepts a single parameter: the service point to instantiate.
To create a singleton service, the instantiate
method should cache the new service, returning it (if it exists) instead of instantiating a new service.
Lastly, be aware that you may need to do some synchronization via mutexes to make your new service model thread-safe. Multiple threads could call it concurrently, which may cause problems if your model stores any state (as is the case with models that enforce a singleton model of service instantiation).
Here’s a brief example of a custom service model, pulled off the top of my head:
class FiveMinuteServiceModel < Copland::Instantiator::Abstract CachedService = Struct.new( :service, :time ) def initialize( point, definition ) super @mutex = Mutex.new @service_cache = Hash.new end def instantiate @mutex.synchronize do svc = @service_cache[ point ] if svc.nil? || Time.now - svc.time > 300 svc = CachedService.new( point.instantiate, Time.now ) @service_cache[ point ] = svc end return svc.service end end register_as "custom.five-minutes" end
This (contrived) example will result in a singleton-style model (non-deferred), which will instantiate the requested service point if it has been more than five minutes (300 seconds) since the point was last instantiated. It can be referenced in a service point definition under the model
attribute, as “custom.five-minutes”.