Tom Sugden: The Flexible Configuration Options of Parsley

« Writing Genuinely Reusable Flex Components | Main | How to Unload Modules Effectively »

February 9, 2010

The Flexible Configuration Options of Parsley

One of the nice design decisions taken by Jens Halm when he created the Parsley Application Framework was to separate the configuration mechanism from the core of the framework, so different forms of configuration can be used as required. This idea in itself is not new, since Martin Fowler advocated it in his 2004 paper, "Inversion of Control Containers and the Dependency Injection pattern":

"My advice here is to always provide a way to do all configuration easily with a programmatic interface, and then treat a separate configuration file as an optional feature. You can easily build configuration file handling to use the programmatic interface. If you are writing a component you then leave it up to your user whether to use the programmatic interface, your configuration file format, or to write their own custom configuration file format and tie it into the programmatic interface" - Martin Fowler

The application of this design principle is particularly effective in Parsley. While some frameworks are restricted to specific configuration mechanisms, Parsley provides programmatic interfaces for synchronous and asynchronous configuration, and several out-of-the-box implementations. These interfaces provide an extension-point so developers can plug-in their own configuration processors when the need arises.

Configuration Processors

In Parsley, a configuration processor uses some form of configuration data to build up a registry of object definitions. This process is abstracted by the following interfaces:

Parsley has a number of standard implementations, including the following two most commonly used:

The developer manual has a section explaining how to extend the framework with a new configuration processor:

Example: Modular Configuation Processor

There are various reasons to write a custom configuration processor. Perhaps you want to support your own particular configuration files, loaded and processed at runtime. However, these interfaces open up some other doors for more interesting forms of configuration. For example, they can be used to process configuration data from a compiled module.

Consider a large, modular application. Let's say the application consists of a Flex shell application that loads 20 modules, and 10 of these rely on the same set of shared services. It's undesirable to compile these services into the shell application, where they could be inherited by the modules, since the shell should have no knowledge of these lower level details. Instead they could be compiled into a module and the shell application could load that module at start-up, so the services are available for inheritance, but there is no dependency imposed on the shell.

This can be achieved quite simply by writing a new configuration processor, something like this:

package com.adobe
{
     import flash.events.ErrorEvent;
     import flash.events.Event;
     import flash.events.EventDispatcher;

     import mx.events.ModuleEvent;
     import mx.modules.IModuleInfo;
     import mx.modules.ModuleManager;
     import mx.utils.StringUtil;

     import org.spicefactory.parsley.core.builder.AsyncConfigurationProcessor;
     import org.spicefactory.parsley.core.builder.ConfigurationProcessor;
     import org.spicefactory.parsley.core.registry.ObjectDefinitionRegistry;

     public class ModularConfigurationProcessor 
          extends EventDispatcher 
          implements AsyncConfigurationProcessor
     {
          private static const MODULE_LOADING_ERROR : String = 
               "Unable to load the module at URL {0} due to {1}";
          private static const MODULE_INCOMPATIBLE_ERROR : String = 
               "The module doesn't implement the ConfigurationProcessor interface.";

          private var url : String;
          private var module : IModuleInfo;
          private var registry : ObjectDefinitionRegistry;

          public function ModularConfigurationProcessor( url : String )
          {
               this.url = url;
          }

          public function cancel() : void
          {
               module.removeEventListener( ModuleEvent.READY, moduleReadyHandler );
               module.removeEventListener( ModuleEvent.ERROR, moduleErrorHandler );
          }

          public function processConfiguration(
               registry : ObjectDefinitionRegistry ) : void
          {
               this.registry = registry;
               module = ModuleManager.getModule( url );
               module.addEventListener( ModuleEvent.READY, moduleReadyHandler );
               module.addEventListener( ModuleEvent.ERROR, moduleErrorHandler );
               module.load( registry.domain );
          }

          private function moduleReadyHandler( event : ModuleEvent ) : void
          {
               try
               {
                    processConfigurationWithModule();
                    dispatchEvent( new Event( Event.COMPLETE ) );
               }
               catch ( e : Error )
               {
                    dispatchErrorEvent( e.message );
               }

          }

          private function processConfigurationWithModule() : void
          {
               var instance : Object = module.factory.create();

               if ( instance is ConfigurationProcessor )
               {
                    ConfigurationProcessor( instance ).processConfiguration( registry );
               }
               else
               {
                    throw new Error( MODULE_INCOMPATIBLE_ERROR );
               }
          }

          private function moduleErrorHandler( event : ModuleEvent ) : void
          {
               dispatchErrorEvent( MODULE_LOADING_ERROR, url, event.errorText );
          }

          private function dispatchErrorEvent( message : String, ... rest ) : void
          {
               dispatchEvent( new ErrorEvent(
                    ErrorEvent.ERROR,
                    false,
                    false,
                    StringUtil.substitute( message, rest ) );
          }
     }
}

The processor is initialized with the module URL. It loads the module, creates an instance, then checks whether the module itself is a configuration processor. If so, it delegates configuration processing to the module. Here's an example module:

package com.adobe
{
    import mx.modules.ModuleBase;

    import org.spicefactory.parsley.asconfig.processor.ActionScriptConfigurationProcessor;
    import org.spicefactory.parsley.core.builder.ConfigurationProcessor;
    import org.spicefactory.parsley.core.registry.ObjectDefinitionRegistry;

    public class MyModule extends ModuleBase implements ConfigurationProcessor
    {
        public function processConfiguration(
            registry : ObjectDefinitionRegistry ) : void
        {
            new ActionScriptConfigurationProcessor(
                [ MyModuleConfiguration ] ).processConfiguration( registry );
        }
    }
}

Parsley's extension points can be taken a little further by writing a complementary configuration tag:

package com.adobe
{
    public class ModularConfig implements ContextBuilderProcessor 
    {
        public var url : String;

        public function processBuilder( builder : CompositeContextBuilder ) : void 
        {
            builder.addProcessor(
                new ModularConfigurationProcessor( url ) );
        }
    }
}

So now a modular configuration can be easily combined with other forms of Parsley configuration using the usual MXML tags:

<mx:Application ... xmlns:sf="http://www.spicefactory.org/parsley">

    <sf:ContextBuilder>
        <sf:FlexConfig type="{ MyShellApplicationConfig }"/>
        <adobe:ModularConfig url="MyModularConfig.swf"/>
        <adobe:ModularConfig url="MyOtherModularConfig.swf"/>
    </sfConfigBuilder>

    ...

</mx:Application>

Here the shell application configuration will be combined with the configuration from two modules to form a Parsley context that can be inherited by other modules, loaded later on-demand.

Conclusion

When creating a framework, it is wise to define generic interfaces for configuration processing, so that different formats can be used where appropriate. In many cases programmatic configuration with MXML is the simplest and most desirable option, but there are several valid cases for configuration from XML and other kinds of file (including SWFs) loaded at runtime. The configuration interfaces provided by Parsley satisfy this requirement very well.

Posted by tsugden at February 9, 2010 10:25 PM

Comments

Post a comment




Remember Me?

(you may use HTML tags for style)