Tutorial #5. Configuration Points

The sources for this tutorial may be found in the tutorial/05 directory of the Copland distribution.

Introduction

Suppose, next, we want to add the ability to register new functions with the calculator, so that third parties that wish to reuse our snappy little number cruncher can add their own custom operations.

Think for a minute how you would solve this. There are several approaches that could be taken, none of them necessarily any better or worse than the other. The drawback for each of them is that you would have to implement the infrastructure yourself.

Copland provides a ready-made package-centric configuration infrastructure, which (as you will see) allows any package to contribute configuration data to configuration points in any other package.

Steps

  1. Modify Calculator

    First, let’s modify our Calculator class again. We’ll add another writer attribute, called functions, which we’ll assume will always be a Hash (or Hash-like) object. Each pair in the hash will be a named object that implements the :compute message.

      class Calculator
        attr_writer :adder
        attr_writer :subtractor
        ...
        attr_writer :functions
    

    Then, we’ll implement a new method on Calculator, called function, which allows clients to specify a function to execute and the arguments to give it. We won’t bother with error checking:

      def function( name, *arguments )
        @functions[ name ].compute( *arguments )
      end
    
  2. Create a Configuration Point

    Next, we’ll create a configuration point. A configuration point may be either a list, or a map. We want a map, so that it will work nicely as a lookup for function services.

    Configuration points are defined in their own section of the package descriptor, under the configuration-points key. Thus:

      id: tutorial
    
      service-points:
        ...
    
      configuration-points:
    
        CalculatorFunctions:
          type: map
    

    This creates a new (and empty) configuration point, called CalculatorFunctions. It is of type “map”, which means it quacks like a Hash. Since it belongs to the tutorial package, its fully qualified name is “tutorial.CalculatorFunctions”.

    For now, we’ll leave it empty. We’ll contribute values to it in a little bit.

  3. Edit the Calculator Service Point

    Now, we edit the service point that defines our Calculator service. Specifically, we add another property initializer:

      Calculator:
        model: prototype
        implementor:
          factory: copland.BuilderFactory
          class: tutorial/Calculator
          properties:
            adder: !!service Adder
            ...
            functions: !!configuration CalculatorFunctions
    

    Here, we’re telling Copland to associate the CalculatorFunctions configuration point with the functions property of our Calculator.

    This means that anything any package adds to that configuration point is going to be visible to our Calculator, via its functions property. The only part we’re missing, then, is what goes into that configuration point.

  4. Prepare to Create the tutorial.functions Package

    Just to keep things nice and neat, we’ll create another package in which we’ll define our “custom” calculator functions.

    Create a new subdirectory in the same directory as your calculator implementation. Call it functions. Then change to that directory.

      $ mkdir functions
      $ cd functions
    
  5. Implement Some Functions

    We need to decide which custom functions to implement. I’ll arbitrarily recommend the trigonometric “sin” function, and the natural logarithm, mostly because they’re fun to say, but also because they’re two of many available functions that Ruby already provides us implementations for.

    So, let’s create a “services.rb” file in our new “functions” subdirectory. We’ll implement two classes, one for each new function. Each class only has to respond to the “compute” message, accepting the expected parameters and returning the computed result. Thus:

      class Sine
    
        def compute( a )
          Math.sin( a )
        end
    
      end
    
      class NaturalLogarithm
    
        def compute( n )
          Math.log( n )
        end
    
      end
    
  6. Define the Service Points

    Now, we create the package descriptor for our new package. Create a new file called “package.yml” in the “functions” subdirectory:

      ---
      id: tutorial.functions
    
      service-points:
    
        Sine:
          implementor: functions/services/Sine
    
        NaturalLogarithm:
          implementor: functions/services/NaturalLogarithm
    

    Note the name of the package: tutorial.functions. This is the recommended way of naming your packages, with names of subpackages including the names of their ancestor packages. However, you are in no way constrained to name them this way—Copland does not enforce it.

  7. Contribute the Services to the Configuration Point

    Next, we tie it all together. Our tutorial.functions package needs to contribute the two services it defines to the tutorial.CalculatorFunctions configuration point. This way, when the Calculator service is instantiated, it will come ready-made with a map of all functions that other packages want to be made available.

    Contributes are made in the contributions section of the package descriptor. Just specify the name of the configuration point to contribute to, and then the values you want to contribute. For configuration points of type list, the values should be formatted as a list. For maps, the values should be formatted as a map.

      id: tutorial.functions
    
      service-points:
        ...
    
      contributions:
    
        tutorial.CalculatorFunctions:
          sin: !!service Sine
          ln: !!service NaturalLogarithm
    

    Here we’ve registered the Sine service under the name “sin”, and the NaturalLogarithm service under the name “ln”.

  8. Put it Together

    Now, in our “main.rb” driver file, we just need to call our new function interface and see if it really works:

      require 'copland'
    
      registry = Copland::Registry.build
    
      calc = registry.service( "tutorial.Calculator" )
    
      puts "sin(pi/4) = #{calc.function( "sin", Math::PI/4 )}" 
      puts "ln(e^3) = #{calc.function( "ln", Math::E ** 3 )}" 
    
      registry.shutdown
    

    If all was done correctly, you should see the following output:

      $ ruby main.rb
      sin(pi/4) = 0.707106781186547
      ln(e^3) = 3.0
    

Summary

The following techniques were demonstrated in this tutorial:

  1. Applications with multiple Copland packages
  2. Creating a configuration point
  3. Contributing to a configuration point
  4. Associating a configuration point with a property of a service

Additionally, you saw that subdirectories would (by default) be recursively searched for package descriptors.

Play with this tutorial some more. Try adding some more functions. Try adding some more packages that contribute to the tutorial.CalculatorFunctions configuration point. The more you use these features, the better you’ll understand them.