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
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:computemessage.class Calculator attr_writer :adder attr_writer :subtractor ... attr_writer :functionsThen, 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 ) endCreate 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-pointskey. Thus:id: tutorial service-points: ... configuration-points: CalculatorFunctions: type: mapThis 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.
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 CalculatorFunctionsHere, we’re telling Copland to associate the CalculatorFunctions configuration point with the
functionsproperty of our Calculator.This means that anything any package adds to that configuration point is going to be visible to our Calculator, via its
functionsproperty. The only part we’re missing, then, is what goes into that configuration point.Prepare to Create the
tutorial.functionsPackageJust 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
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 endDefine 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/NaturalLogarithmNote 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.Contribute the Services to the Configuration Point
Next, we tie it all together. Our
tutorial.functionspackage needs to contribute the two services it defines to thetutorial.CalculatorFunctionsconfiguration 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
contributionssection 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 NaturalLogarithmHere we’ve registered the Sine service under the name “sin”, and the NaturalLogarithm service under the name “ln”.
Put it Together
Now, in our “main.rb” driver file, we just need to call our new
functioninterface 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.shutdownIf 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:
- Applications with multiple Copland packages
- Creating a configuration point
- Contributing to a configuration point
- 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.