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

  1. 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.

  2. 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
    
  3. 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?!?

  4. 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 the singleton and singleton-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?

  5. 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 the calc.Calculator service to return a new instance of the Calculator class. Just set the model by specifying the model element in your service point descriptor (in the package.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:

  1. The default service model for a service is singleton-deferred. This model (and it’s cousin, the singleton service model) both cause a service point to be instantiated only the first time the service is requested.
  2. 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.