Tutorial #3. Service Models
The sources for this tutorial may be found in the tutorial/03 directory of the Copland distribution.
Introduction
Let’s take our calculator example one step further and add a “memory” function. By storing a value into the memory, you can then call the various operators with only one parameter, and have them operate on the remembered value.
Sound cool? It’s also pretty easy! Here goes…
Steps
Add the Memory
First, we need to modify the calculator to hack in support for memory. This involves adding an instance variable and two methods, getters and setters for that variable:
attr_accessor :memory
We don’t worry about initializing the corresponding instance variable in the code—that’s more properly a task for the IoC container. We just edit the “package.yml” file and add another property initializer to the Calculator service:
Calculator: implementor: factory: copland.BuilderFactory class: tutorial/Calculator properties: adder: !!service Adder subtractor: !!service Subtractor divider: !!service Divider multiplier: !!service Multiplier memory: 0
This then says then when constructing a Calculator service, always set the memory property to 0.
Modify the Operators
Next, we need to modify the operators so that they each treat the second parameter as optional, defaulting to our memory variable:
def add( a, b=memory ) @adder.add( a, b ) end def subtract( a, b=memory ) @subtractor.subtract( a, b ) end def multiply( a, b=memory ) @multiplier.multiply( a, b ) end def divide( a, b=memory ) @divider.divide( a, b ) end
Play
We can now try it by doing multiples and powers of 5. Edit our “main.rb” driver file to look something like this:
require 'copland' registry = Copland::Registry.build calc = registry.service( "tutorial.Calculator" ) puts "Multiples of 5:" 10.times do puts calc.memory calc.memory = calc.add( 5 ) end calc.memory = 1.0 puts puts "Powers of 5:" 10.times do puts calc.memory calc.memory = calc.multiply( 5 ) end
When run, it should spit out a list of the multiples of five, and the powers of five! Slick, huh?
But wait! Let’s try another trick—let’s get TWO calculators going at once and have them each dealing with a different base. Should be pretty straightforward to do; perhaps something like this:
require 'copland' registry = Copland::Registry.build calc1 = registry.service( "tutorial.Calculator" ) calc2 = registry.service( "tutorial.Calculator" ) puts "Multiples:" 10.times do puts "#{calc1.memory}\t#{calc2.memory}" calc1.memory = calc1.add( 5 ) calc2.memory = calc2.add( 2 ) end calc1.memory = 1.0 calc2.memory = 1.0 puts puts "Powers:" 10.times do puts "#{calc1.memory}\t#{calc2.memory}" calc1.memory = calc1.multiply( 5 ) calc2.memory = calc2.multiply( 2 ) end
We save it, run it, and—what?!? We’re getting multiples of 7 and powers of 10, for each column… Huh?!?
Diagnosing the Problem
What’s happening is this: because you haven’t specified a service model for the Calculator service point, the default model (
singleton-deferred
) is being used. Both thesingleton
andsingleton-deferred
models are alike in that anytime you request an instance of a service point that uses one of those models, you get the exact same instance back. Just like “instantiating” a singleton class.That’s right. When we requested two instances of the Calculator service, we were actually getting two references to the same instance, instead. Thus, each iteration through the “multiples” loop was adding 5+2=7 to the memory, and each iteration through the “powers” loop was multiply 5*2=10 by the memory.
Huh. Okay, so we know the problem. How do we get the behavior we wanted?
Fixing the Problem
So we understand why the problem is happening. But how do we fix it?
Well, first let’s understand about service models. The service model is what controls when a service point is instantiated. For the singleton models, the service point is only instantiated once—the first time it is requested. What we want is a service model that will instantiate the service point every time we request it.
We’re in luck. There is just such a service model: prototype. By setting the service model to
prototype
, we’ll cause each request for thecalc.Calculator
service to return a new instance of the Calculator class. Just set the model by specifying themodel
element in your service point descriptor (in thepackage.yml
file):Calculator: model: prototype implementor: ...
Save your file, and run your “main.rb” script again—the output should look much better!
Summary
In this tutorial, you learned:
- The default service model for a service is
singleton-deferred
. This model (and it’s cousin, thesingleton
service model) both cause a service point to be instantiated only the first time the service is requested. - To get a service point that is instantiated every time it is requested, use the
prototype
service model.
In general, use the prototype
model (or possibly the threaded
model) when you have a service that remembers some kind of state. For stateless services, the singleton
models are appropriate.
Some models are deferring models. This means that the service is not actually constructed until the first time a method is invoked on the new service. The difference is subtle, but important: singleton
and singleton-deferred
are identical, except with singleton
the service is constructed as soon as it is requested, whereas with singleton-deferred
, the service won’t actually be constructed until the last possible moment, when you need to invoke a method of the service.
Likewise, there is a prototype-deferred
model, to compliment the prototype
model.
The only other standard service model is the threaded
model, which constructs a new instance of the service for each thread that requests it. In that way, it is like a cross between the singleton
and prototype
models.