/// Wednesday, November 9

Dependency Injection and Scripted Configuration for Enterprise Development - Part 2

Nano and a Configuration Script - Thinking Outside the Box

In this part of the article series, I discuss the external configuration file NanoContainer compiles and invokes to create a managed container of objects. An application retrieves objects from this managed container to perform business logic. These objects are not necessarily instantiated. They may be lazy-loaded for performance and resource considerations.

To get a better idea how Nano integrates with an application, consider this sequence diagram:

  1. Administrator starts an application.
  2. Application asks Nano to create a container.
  3. Nano compiles the configuration script.
  4. Nano sets IScript.Parent to a default container.
  5. Nano asks the script to Compose() the container.
  6. Script returns a container.
  7. Nano returns the container to the application.
  8. Application requests objects from the container and uses them to perform busines logic.

Pretty simple, eh? It's worth reiterating that Nano can use a script written in practically any .NET supported language---Boo, C#, VB, JS, JavaScript, VBScript or even an XML configuration file. Pick your poison. One benefit of using Boo (other than it's more elegant than the other .NET languages) is that a Boo install has an interpreter which may be used to run the script independent of the application! Nano gives a team the full power of any .NET language to configure an application. At first this may not seem to be much of an advantage. Think outside of the box! What if a a configuration file could test itself? What if a configuration file could perform a check on its environment before running an application? What if a configuration file could test for missing property values? What if a configuration file could swap in mock objects? A scripted configuration file's behaviour is only limited by your coding skills. Before we go into the actual configuration file, consider the class diagram for this configuration block.

Class

Purpose

ConfigBase

Is the base class for all configuration files. The class contains methods to check for missing keys, test settings any other future features. It also contains a Settings hash to hold objects derived from SettingBase.

SettingBase

Is the base class for all settings. This class defines an abstract test method that must be implemented.

RequiredSetting

Defines a setting that is required. When Test() is executed it always throws an exception. Must be replaced by another setting.

StringSetting

Defines a string setting. Test() simply returns with no side effect

SafeCreateDirSetting

Safely creates a directory. Test() creates the directory if it does not exist. It's assumed Test() will be called once.

SqlConnectionStringSetting

Defines a SqlConnection string. Test() will attempt to open a connection to the database.

BooConfig

Is the external script file. It does not reside in the ConfigBlock assembly.

The complete source of the configuration file follows:

# BooConfig.boo - compiled by NanoContainer when an application starts.
01 class BooConfig(ConfigBase):
02     # Gets or sets check mode.
03     [property(IsCheck)]
04     _isCheck = false
05
06     # Populates the master settings hash.
07     override def PopulateSettings():
08         Settings = {
09             "name" : StringSetting("mgutz"),
10             "connectionString" : RequiredSetting(),
11             "logDirectory" : SafeCreateDirSetting("/temp/log")
12         }
13
14     # Set the environment for DEV.
15     private def DEV():
16         Settings["connectionString"] = SqlConnectionStringSetting("Server=(local);Database=devapp;uid=boodev;pwd=boodev")
17
18     # Set the envrionment for TEST.
19     private def TEST():
20         pass
21
22     # Set the environment for PROD.
23     private def PROD():
24         Settings["connectionString"] = SqlConnectionStringSetting("Server=(local);Database=prodapp;uid=booprod;pwd=booprod")
25
26     # Composes the object container used by an application.
27     # This method is invoked by an application.
28     override def Compose() as IMutablePicoContainer:
29         _container = DefaultPicoContainer(Parent)
30
31         # change to match environment, could be automated by pinging a unique serverr
32         EnvironmentMethod = DEV
33         ConfigureEnvironment(_isCheck)
34
35         # register an instance of an object
36         _container.RegisterComponentInstance("name", Settings["name"].ToString())
37
38         # lazy-load a connection object by registering its implementation not instance
39         _container.RegisterComponentInstance("dbConnectionString", Settings["connectionString"].ToString())
40         params = array (IParameter, (ComponentParameter("dbConnectionString"),))
41         _container.RegisterComponentImplementation("dbConnection", SqlConnection, params)
42
43         # return the container
44         return _container
45
46 # A check on the environment is performed when this script is called from
47 # booi interpreter. When this script compiled by Nano, this method is not invoked.
48 # These are the same steps taken by the NanoBoo contaienr except the .IsCheck
49 # attribute is not set.
50 def main():
51     config = BooConfig()
52     config.Parent = DefaultPicoContainer()
53     config.IsCheck = true
54     config.Compose()
55
56 main()

The Boo file contains a single class BooConfig derived from ConfigBase. Line 32 must be changed based on which environment the application is deployed. This could be automated by pinging a unique server in each environment. If the server pings back, use the appropriate environment method. Additionally, the file contains a main() method that is executed when the file is run from the command line:

> booi BooConfig.boo

So, what is happening?

First, excuse the sequence diagram. Apparently my UML CASE tool can't handle wierd sequences. The lifeline of the methods should be continous as all of this is actioned in one call to Compose().

  1. Nano, if application is running, or booi, if run from command line, invokes Compose() to create a container.
  2. Script file creates a container to hold objects it builds.
  3. Script file sets the environment method callback.
  4. Script file asks ConfigBase to configure the environment.
  5. ConfigBase in turn asks the script file to populate the Settings hash.
  6. ConfigBase then invokes the environment method, say DEV() to use settings specific to the DEV environment.
  7. If checkEnvironment was true, ConfigBase checks for missing keys by iterating over the hash looking for null or RequiredSetting() objects.
  8. If checkEnvironment was true, ConfigBase iterates over the Settings hash invoking SettingBase.Test() on each object. This tests database connections, missing directories, web service connections, etc.
  9. Flow returns to the script file, where it nows registers implementations and instance of objects storing them into the container previously built. See lines 35-41 where objects are inserted into the container.
  10. The container is returned ending the sequence diagram.

As you can see, a script configuration file can be much more than static key and value pairs. With a little bit of thought, it's very easy to create a configuration block of your own that goes far beyond the Microsoft Enterprise Configuration Block.This article described a simple, yet powerful configuration block that:

The block could easily be extended by creating additional SettingBase-derived classes. Some examples include:

To be continued...

Indeed this is just the tip of the iceberg. I intend to create a Utility class to faciliate use of PicoContainer. It's just too much typing and looks more complicated than it really is. In the next part, I'll cover a few scenarios describing the tangible benefits of NanoContainer in testing and refactoring.

Comments:

Post a Comment



<< Home

This page is powered by Blogger. Isn't yours?