On my current project we’re building multiple websites that share the majority of their logic but each have requirements relevant only to their target audience. This means we need a way to configure the common elements to behave appropriately to each scenario. This post discusses the mechanism I’ve chosen to do this.
In building this mechanism I had a number of goals:
- Minimal Duplication It was not acceptable to simply duplicate whatever logic needed to vary. Whatever mechanism I chose has to keep as much logic common as possible to constraint development cost and ensure consistency of operation in the common elements.
- Low Coupling Most of the elements involved have no need to know about the specific sites and their requirements. These elements do need to know configuration values for their elements but this does not imply that they need to know how decisions for each site are made. This would tie each element to the current sites and require extensive modification if the feature distribution changes or new sites are added.
- Simplicity As this mechanism will be used throughout the system it needs to be simple to adopt so as not to negatively impact the maintainability of the system. Additionally it should not make the system difficult to deploy or run.
- Robustness The system should be reliable and not prone to the introduction of errors. In particular it should not increase the risk of errors at deployment.
- Testable As a strong proponent of TDD I need a solution that will support testing the elements behaviour in each scenario.
- Low Effort I’m looking for a mechanism that doesn’t take significant amounts of time to use, either during development or at deployment.
I also have an advantage in that I can make the decisions at compile time. In this scenario I don’t need the ability to dynamically change these behaviours. This means I don’t actually need to configure things in the sense of having an XML configuration file or similar. Configuration is needed but this may occur at compile time.
I’ve previously discussed making configuration using the standard .NET infrastructure injectable. This does some of what I need, but it still requires XML configuration files which introduce significant complexity and risk to deployments. However I can get where I need to go by keeping the interfaces and changing the implementation.
In my previous usage the code needing configuration has no dependency on the .NET configuration infrastructure. It happens to be provided an implementation that uses it, but this is not necessary. Hence I define interfaces that provide the necessary configuration options. For each site I provide implementations of the interface that supply configuration values relevant to that site. The dependency injection configuration for each site is configured to supply the implementations relevant to that site, and I have a complete solution that meets all my goals.
We start with a configuration interface that may look something like:
internal interface ISiteConfiguration
{
bool SomeConfigurationValue { get; }
bool SomeOtherConfigurationValue { get; }
}
We can start with a single interface and potentially break it apart as the number of properties grows.
public class SomeSiteConfiguration : ISiteConfiguration
{
public bool SomeConfigurationValue
{
get { return false; }
}
public bool SomeOtherConfigurationValue
{
get { return true; }
}
}
The implementation above provides fixed values for each of these configuration properties. There is nothing here to get wrong an deployment time, I can’t accidentally misconfigure a feature with an inappropriate configuration setting. I’m looking here to provide a different configuration between sites and other sites get supplied a different implementation of ISiteConfiguration. This means that I can have some confidence that the set of configuration values will always be consistent.
It is not mandatory to provide fixed values. The properties may apply logic to determine the configuration value, with the different implementations allowing this logic to be varied between uses (or to support fixed values in some places and calculated in others).
In the site dependency injection configuration I then specify the appropriate implementation for that site. If I’m using a container that supports configuration in code I can have things such that the implementation to use is baked into the site and there is no configuration to go wrong between deployments. For instance with StructureMap I could do something like:
ObjectFactory.Initialize(initialise =>
{
initialise.ForRequestedType<ISiteConfiguration>().TheDefaultIsConcreteType<SomeSiteConfiguration>();
});
So how does this measure up to my critieria:
- Minimal Duplication All the instances of a configuration decision are collected into a single location. With some refactoring duplication can essentially be eliminated.
- Low Coupling There is an abstraction between the use of the configuration and the making of the configuration decisions.
- Simplicity Using the configuration is as simple as injecting the configuration interface which is trivial with most containers. As demonstrated above there is nothing to configure at deployment which is about as simple as you can get.
- Robustness The solution ensures configuration decisions are provided, otherwise the system will fail quickly in development. At deployment there is nothing to go wrong as the system is already configured.
- Testable The configuration interfaces are easily mocked for testing purposes.
- Low Effort This utilises my existing DI setup and involves relatively trivial interfaces and implementations.
I’m therefore pretty happy with this system. It can be seen that it is basically the Strategy pattern as applied to configuration, with the extra goodness that it comes pre-configured at deployment.
I’ve discussed this in the context of multiple websites as that is the current scenario I’m dealing with, but this approach may be applied whenever multiple executables share common logic requiring configuration that may be defined at development. If you introduced some configuration for selecting configuration implementations only you could also adapt this to providing fixed sets of configuration for the same executable in multiple environments. This would maintain the majority of the benefits (consistent configuration sets, elimination of the majority of configuration values) for a relatively low cost in deployment effort and risk.