Encodo’s configuration library for Quino: part I
In this article, I’ll continue the discussion about configuration improvements mentioned in the release notes for Quino 2.0-beta1. With beta2 development underway, I thought I’d share some more of the thought process behind the forthcoming changes.
what sort of patterns integrate and customize the functionality of libraries in an application?
An application comprises multiple tasks, only some of which are part of that application’s actual domain. For those parts not in the application domain, software developers use libraries. A library captures a pattern or a particular way of doing something, making it available through an abstraction. These simplify and smooth away detail irrelevant to the application.
A runtime and its standard libraries provide many such abstractions: for reading/writing files, connecting to networks and so on. Third-party libraries provide others, like logging, IOC, task-scheduling and more.
Because Encodo’s been writing software for a long time, we have a lot of patterns that we’ve come up with for our applications. These libraries are split into two main groups:
- Encodo.*: extensions to the .NET framework or third-party libraries that don’t depend on Quino metadata.
- Quino.*: extensions to the .NET framework, third-party libraries or Encodo libraries that depend on Quino metadata.
A sort of “meta” library that lies on top of all of this is configuration and startup of applications that use these libraries. That is, what sort of patterns integrate and customize the functionality of libraries in an application?
Balancing K.I.S.S. and D.R.Y
Almost nowhere in an application is the balance between K.I.S.S. and D.R.Y. more difficult to maintain than in configuration and startup.
So if we already know all of that, why does Quino need a new configuration library?
As mentioned above, there is a lot of commonality between applications in this area. An application will definitely want to incorporate such common configuration from a library. Updates and improvements to that library will then be applied as for any other. This is a good thing.
However, an application will also want to be able to tweak almost any given facet of this shared configuration. That is: just keep the good parts, have those upgraded when they’re changed, but apply customization and extend functionality for the application’s domain. Easy, right?
It is here that a good configuration library will find just the right level of granularity for customization. Too coarse? Then an application ends up throwing out too much common configuration in order to customize a small part of it. Too fine? Then the configuration system is too verbose or complex and the application avoids using it.
Instead, a configuration system should establish clear patterns—optimally, just one—for how to apply customization.
- The builder of the underlying configuration library has to consider the myriad situations that might face a library developer and distill those requirements to a common pattern.
- The library developer needs to think about which parts an application might want to customize and think about how to expose them.
So if we already know all of that, then why does Quino need a new configuration library? Well…
History of Quino’s Configuration Library
It’s really easy to make things over-complicated and muddy. It’s really easy to end up growing several different kinds of extension systems over the years. Quino ended up with a generics-heavy API that made declaring new configuration components very wordy.
The core of Quino is the metadata definition for an application domain. That part has barely changed at all since we first wrote it lo so many years ago. We declared it to be our core business—the part that we are better than others at—the part we wanted to have under our own control. Our first draft has held up remarkably well.
Many of the other components have undergone quite a bit of flux: changes in requirements and the components themselves as well as new development processes and patterns all contributed to change. Over time, various applications had different needs and made adjustments to a different iteration of the configuration library. We moved from supporting only single-threaded, single-user desktop applications to also supporting multi-user, multi-threaded services and web servers.
…we were left with an ugly configuration system that no-one wanted to extend or
use—so yet another would be invented.
For all of these different applications, we naturally wanted to maintain the common configuration where possible—but customizations for new platforms stretched the capabilities of the configuration library.
Customization would be made to a new version of that library, but applications that couldn’t be upgraded immediately forced backwards-compatibility and thus resulted in several different concurrent ways of configuring a particular facet of an application.
In order to keep things in one place, we ended up breaking the interface-separation rule. Dependencies started clumping drastically, but it was OK because nobody was trying to use one thing without the other ten. But it was hard to see what was going on; customization became a black box for all but one or two gurus. On and on it went, until we were left with an ugly configuration system that no-one wanted to extend or use—so yet another would be invented, ad-hoc. And so it went.
Principles for Quino 2.0 Configuration
With Quino 2.0, we examined the existing system and came up with a list of principles.
- Consistency: there should be only be one way of customizing settings and components. When a developer asks how to change something, the answer should always be the same pattern. If not, there better be a damned good reason (see “Configuration vs. Execution” below).
- Opt-in configuration: No more magic methods or base classes that automatically add components and settings in black boxes. Even if the application has to call one or two more methods, it’s better to be declarative than clever™.
- Inversion of Control: Standardize configuration to use an IOC container or service locator wherever possible. Instead of clumping settings in configuration or application objects, create discrete settings and put them in the container. Make dependencies explicit (constructor parameters!) and resolved through the container wherever possible.
- Configuration vs. Execution: Be very aware of the difference between the “configuration” phase and the “execution” phase. During configuration, the service locator is used in write-only mode; during execution, the service locator is in read-only mode. Code executed during configuration must rely only on explicit dependency-injection via constructor.
- Common Usage: Establish a pattern for calling configuration methods, from least to most specific. E.g. call Quino’s base configuration methods before any application-specific customization. Establish patterns for how to configure a single startup action or how to create settings for a larger component that could be further customized in subsequent phases.
In the next part, we’ll take a look at some concrete examples and documentation for the new patterns.