Previous Up Next

Chapter 10  Best Practices

10.1  Introduction

Much of this manual is concerned on what you can do with Config4*. This chapter and the following ones are concerned with what you should do. This chapter offers advice on best practices that can help you use Config4* effectively.

10.2  Use a Top-level Scope for Each Application

Before you started using Config4*, you probably thought, “My application has a configuration file.” Now that you are using Config4*, you should adjust your thinking to be, “My application has a scope within a configuration file.” By doing this, you make it possible for several applications to share one configuration file. In practice, many people will be content to have a separate configuration file for each application they use. However, it is easy to imagine some people preferring to combine the configuration of several (presumably related) applications in one configuration file.

Some other configuration technologies, such as XML and Java properties files, do not provide good support for one file to contain information for several applications. If your have prior experience with one of these technologies, then it may seem strange at first to confine your application to one scope within a configuration file, but you will soon get used to it and appreciate its flexibility. At the other extreme is the Windows Registry, which is a configuration source for all applications running on a PC. Many people dislike the monolithic nature of the Windows Registry and they may feel wary of sharing one configuration file between multiple applications. I am not advocating that all the applications on a computer must share a single configuration file. Rather, I am saying that developers should not force users to adopt a particular granularity of configuration, such as a separate file for each application or one file shared by all applications. It is better to leave the choice of granularity to the users.

When using Config4*, it is trivial to design an application so it can obtain its configuration from a scope within a configuration file. To do this, you ensure the application can accept command-line arguments that specify both the configuration file and its scope within that file.

myApp.exe -cfg foo.cfg -scope foo

Then you use the values obtained from those command-line arguments as parameters when calling parse() and the lookup-style operations.

cfg = Configuration::create();
cfg.parse(cfgFile);
logDir = cfg.lookupString(scope, "log.dir");

It is as simple as that.

By the way, the command-line options do not have to be called -cfg and -scope; those names are used just as examples. Also, you might prefer to use something other than command-line options, such as environment variables or the Windows Registry, to get the name of the configuration file and the scope within it.

Perhaps an application will default to using its own name as its configuration scope so users are not forced to specify the -scope option all the time. If so, then it is important that this is a default name for the scope rather than a hard-coded name. This is because some users may wish to create several configurations for an application. For example, a user might create scopes called foo-no-diagnostics and foo-with-diagnostics for an application called Foo.

10.3  Naming Convention for Variables

If your application has only a few configuration variables, then you do not need to put much thought into a naming convention for those variables. However, the number of configuration variables used by your application is likely to increase over time. If your application consists of some core functionality plus optional plug-ins (perhaps packaged as UNIX shared libraries, Windows DLLs or Java classes), then you are likely to need configuration variables not just for the core functionality of the application but also for each of its optional plug-ins. For example, I know of a product with plug-ins that has over 600 configuration variables. Most of those configuration variables have sensible default values, so users do not need to be concerned with that vast quantity of configuration variables. However, the developers of that product had to use a consistent naming scheme to ensure that the names of configuration variables used by one plug-in did not clash with the names of configuration variables used by other plug-ins.

Regardless of whether your application is split over several plug-ins or is packaged as one monolythic executable, your application can be thought of as a collection of logical subsystems. The way to avoid name clashes is to use the name of logical subsystems as prefixes on the names of configuration variables. My preference is to use nested scopes for each logical subsystem within an application, and use the top-level scope for configuration variables that do not neatly fit into any logical subsystem:

foo {
    idle_timeout = "5 minutes";
    log {
        file  = "/tmp/fooSrv.log";
        level = "2";
    }
    database {
        host = "...";
        username = "...";
        password = "...";
    }
}

Some readers may dislike the indentation caused by nested scopes in the above example. and may prefer a flatter “look and feel” to a configuration file. That is easily achieved by using "." (the scoping operator) instead of explicitly opening scopes:

foo {
    idle_timeout = "5 minutes";
    log.file  = "/tmp/fooSrv.log";
    log.level = "2";
    database.host = "...";
    database.username = "...";
    database.password = "...";
}

Within a logical subsystem, you should use a consistent convention for compound names, for example, a_compound_name or aCompoundName.

10.4  Fail-fast Configuration

Fail fast [Sho04] is a principle of software design. Its essence is that when a problem occurs, an application should fail as soon as possible and in a visible manner, for example, by printing an informative error message. This makes it easier to find and fix bugs (or misconfiguration) which, in turn, leads to more robust applications.

At first sight, the fail-fast principle appears to be in conflict with the use of default (or fallback) configuration values. To see why, consider the following statements.

str1 = cfg.lookupString(scope, "log.file");
str2 = cfg.lookupString(scope, "log.file", "foo.log");

If the log.file variable does not exist, then the first statement throws an exception that contains an informative error message. This is in keeping with the fail-fast principle. In contrast, the second statement returns the specified default value. Returning a default value is appropriate behaviour if the configuration variable does not exist. But what if the configuration variable was just misspelt (perhaps as log.File instead of log.file)? In this case, the misspelling goes undetected, so the fail-fast principle is violated and the application writes to the wrong log file.

Thankfully, the schema validation capabilities of Config4* (provided by the SchemaValidator class) can detect misspelt names of configuration variables and so enable you to adhere to the fail-fast principle despite the use of default (or fallback) configuration. You can find an overview of schema validation in Section 3.10, and a more complete discussion in Chapter 9.

10.5  Zero Configuration

Zero configuration is a term used with both software and hardware. It refers to a piece of software or hardware that is configurable, but can work “out of the box” without the need for an explicit configuration step. Zero configuration is prized because it facilitates ease of use.

It is trivial for a Config4*-enabled application to be enabled for zero configuration. This is achieved by the application using fallback configuration (Section 3.6.3) so the application can work without the need for external configuration. This technique is illustrated by some of the demos, which are discussed in Chapter 11.

When writing the fallback configuration for an application, keep in mind that the fallback configuration can make use of the adaptive configuration capabilities of Config4* (Section 2.10). In this way, the fallback configuration can take account of the operating system, hostname and username of the person running the application.

10.6  Schema Validation for Fallback Configuration

Ideally, you should perform schema validation on all sources of configuration information for an application. For example, if you are developing a zero-configuration application, then you should perform schema validation not just on the application’s optional configuration file, but also on its fallback configuration. However, this raises a practical problem, as I now discuss.

I explained in Section 9.2.2, that the identifier rules in a schema can be either @optional or @required; if neither is specified, then the default behaviour is @optional. The problem is that we need every identifier rule to have both the @optional and @required semantics:

To support these conflicting requirements, the validate() operation of the SchemaValidator class can take an optional forceMode parameter that “forces” all the identifier rules to have either the @optional or @required semantics. The use of this parameter is illustrated in Figure 10.1.

Figure 10.1: Specifying a “force mode” for schema validation
 1  String schema[] = new String[] {
 2      "idle_timeout = durationMilliseconds",
 3      "log_level = int[0, 5]",
 4      "log_file = string"
 5  };
 6  String cfgFile = ...; // set from a command-line option
 7  String scope = ...;   // set from a command-line option
 8  Configuration cfg = Configuration.create();
 9  try {
10      if (cfgFile != null) {
11          cfg.parse(cfgFile);
12      }
13      cfg.setFallbackConfiguration(Configuration.INPUT_STRING,
14                                   FallbackConfig.getString());
15      SchemaValidator sv = new SchemaValidator();
16      sv.parseSchema(schema);
17      sv.validate(cfg, scope, "", SchemaValidator.FORCE_OPTIONAL);
18      sv.validate(cfg.getFallbackConfiguration(), "", "",
19                   SchemaValidator.FORCE_REQUIRED);
20  } catch(ConfigurationException ex) {
21      System.out.println(ex.getMessage());
22  }

Schema validation of the application’s configuration file (line 16) uses FORCE_OPTIONAL to ensure that all the identifier rules have the @optional semantics. (Actually, doing this is unnecessary in the example shown because all the identifier rules in the schema (lines 1–5) have @optional semantics by default.) Then, schema validation of the fallback configuration (lines 18–19) uses FORCE_REQUIRED. Doing this ensures that an exception will be thrown if the fallback configuration is incomplete. Such an exception will be noticed during application development, which means that the problem of incomplete fallback configuration can be rectified long before the application is shipped to customers or deployed in a production environment.

10.7  Working with Lists

Config4* provides operations to lookup a string and convert it to another commonly used type. For example, lookupInt() calls lookupString() and tries to convert the returned string to an integer. However, Config4* does not provide operations to obtain a list of strings and convert it to a list of other built-in types. For example, Config4* does not provide lookupListOfInt() or lookupListOfBoolean(). Config4* could provide such operations, but this would not be sufficient because some applications would want to lookup a list of mixed types, for example, a list in which the first element is a string, the next element is an integer, the next a duration and so on. Instead, Config4* provides lower-level functionality that enables developers to write their own “lookup a list of exactly what I want” operations.

Perhaps the easiest way to check that a list variable contains exactly what you want is to use the SchemaValidator class to validate it. However, Config4* does provide operations that enable you to perform validation checks and data-type conversion on individual elements of a list. It is useful to be aware of the existence of these operations, in case you ever need them. Some operations have names of the form is<Type>(), for example, isBoolean() and isInt(). These operations take a string parameter and return a boolean indicating if the supplied string can be converted to the relevant type. Some other operations have names of the form stringTo<Type>(), for example, stringToInt() and stringToBoolean(). Here is the Java signature for one such operation.

int stringToInt(String scope,
                String localName,
                String str) throws(ConfigurationException)

This operation tries to convert the specified string (str) into an integer. If the conversion fails, then the operation throws an exception containing a descriptive error message. For example, if the scope parameter is "foo", the localName parameter is "my_list[3]" and the configuration file previously parsed was called example.cfg, then the message in the exception will be:

example.cfg: Non-integer value for ’foo.my_list[3]’

The intention is that developers will iterate over all the strings within a list and handcraft the localName parameter for each list element to reflect its position within the list: "my_list[1]", "my_list[2]", "my_list[3]" and so on. In this way, the stringTo<Type>() operations can produce informative exception messages if a data-type conversion fails. Note that although many programming languages, including C++ and Java, index arrays starting from 0, you should format the localName parameter so the index starts at 1. This is to be consistent with the error messages produced by the SchemaValidator class.

Lists of mixed types can be used to emulate tables of information. Lists denoting tables are best formatted in the form of a table, with a comment line indicating the meaning of each column.

foo {
    price_list = [
      # product     price
      #-------------------------
        "milk",     "$0.99",
        "flour",    "$2.17",
        "jam",      "$1.42"
    ];
}

Figure 10.2 contains Java code that calls lookupList() to retrive the value of foo.price_list and then processes it row by row. The code calls stringToUnitsWithFloat() to convert a string, for example, "$2.17", into a ValueWithUnits object that provides operations to access the units ("$") and value (2.17).

Figure 10.2: Code to process price_list
String[]          priceList;
String            product;
String            priceStr;
String            localName;
int               i;
int               numRows;
int               row;
final int         numColumns = 2;
ValueWithUnits    vwu;

try {
    priceList = cfg.lookupList(scope, "price_list");
    numRows = priceList.length / numColumns;
    for (i = 0; i < numRows; i++) {
        product   = priceList[i* numColumns + 0];
        priceStr  = priceList[i* numColumns + 1];
        row       = (i / numColumns) + 1;
        vwu       = cfg.stringToUnitsWithFloat(scope,
                          ("price_list["+ row +"].price"),
                          "price", priceStr,
                          new String[]{"$", "EUR"});
        priceCurrency = vwu.getUnits();
        priceAmount   = vwu.getFloatValue();
        process(product, priceCurrency, priceAmount);
    }
} catch(ConfigurationException ex) {
    System.err.println(ex.getMessage());
}

When calling stringToUnitsWithFloat(), the code in Figure 10.2 constructs a value for the localName parameter that reflects the table element being accessed. If the price column in the first row of the table is changed from "$0.99" to "car", then the code in Figure 10.2 will produce the following informative error message:

someFile.cfg: invalid price (’car’) specified for
’foo.price_list[1].price’: should be ’<units> <float>’
where <units> are ’$’, ’EUR’

10.8  Use a Wrapper Class around Config4*

Perhaps you think Config4* is the best configuration technology in existence and you cannot imagine ever wanting to use anything else. However, there is a good chance that within a few years, or even within a few months, you will switch to something else that will not have the same API as Config4*. This something else might be a different configuration technology, or it might be a newer version of Config4* that has a backwards-incompatible API. When you make such a switch, you will have to modify existing code that uses Config4*. Making such modifications can be time consuming if Config4* is used in many places in your application.

You can greatly reduce the impact of moving to a different configuration technology (or upgrading to a newer version of Config4* that has a backwards-incompatible API) by writing your own class that encapsulates the use of Config4*. Some of the demos supplied with Config4* provide examples of such encapsulation. You can find a discussion of the demos in Chapter 11.

10.9  Summary

This chapter has provided advice on best practices for using Config4*. Following the advice will help you to develop applications that are flexible, user-friendly and easy to maintain.


Previous Up Next