Variations in behavior
If you want to buy Microsoft Windows, you can choose from different editions: Home, Pro, Enterprise editions are available. Each edition comes with different features. The remarkable thing is: instead of having thee different editions of installation media, the license key determines which edition will be installed.
If you want to buy Microsoft Visual Studio, the situation is different. Visual Studio also comes with different editions: Community, Professional or Enterprise. But, instead of having a single installation medium, there are different installers per edition. So you need to start the correct installer, otherwise the license key won’t match.
The key thing is that a single software product can be configured to behave differently in different circumstances, and that there are many ways to implement this. This article gives an overview of the ways that software behavior can be adapted based on certain conditions. This has not just to do with deploying multiple editions of the product, but can also be used to implement feature toggles, to perform A/B tests or to enable debugging features.
Read more: Variations in behaviorIf
The most low level variation is an implementation of an if
block. For example:
..
if (Environment.GetEnvironmentVariable("debug") == "true") {
showDebugPanel();
}
In this example, the environment variable debug
is used as feature flag: if it is set to true
, then a debug panel is shown. This type of functionality can be added nearly everywhere. However, it comes with serious drawbacks. It makes the code more complex to read. And, especially when multiple of these flags are implemented, it might cause bugs because the flags may interact with each other.
Strategy pattern
The Strategy design pattern is useful to inject variations of behavior. Using this pattern makes it possible to have multiple implementations of a feature and choose the one to use at runtime. For example:
class MainWindow {
private readonly IPanel[] mPanels;
public MainWindow() {
if(Environment.GetEnvironmentVariable("debug") == "true") {
mPanels = new IPanel[] { new DocumentPanel(), new DebugPanel() };
} else {
mPanels = new IPanel[] { new DocumentPanel() };
}
}
}
The feature flag here is evaluated only once. The debug panel is treated like any other panel, except that it is only created when the feature flag is set. Apart from creation, MainWindow
class has no knowledge of debug panels at all. That makes it much easier to change the debug panel later on.
Dependency Injection
One step further is Dependency injection. Dependency injection means that the dependencies are not maintained by the class itself, but instead they are defined by the code that creates the class. This makes it possible to make the decision even more centralized:
class MainWindow {
public MainWindow(IPanel[] panelsToShow) {
...
}
}
This defines the main window of an application, but instead of letting MainWindow
create its panels itself, they are provided on creation. For example:
public static int Main(string[] args) {
IPanel[] panels;
if(Environment.GetEnvironmentVariable("debug") == "true") {
panels = new IPanel[] { new DocumentPanel(), new DebugPanel() };
} else {
panels = new IPanel[] { new DocumentPanel() };
}
MainWindow window = new MainWindow(panels);
}
There is still an if
statement, but it is evaluated only at the start of the program. The code that runs the application is unaware of the feature flag. It works in a more general manner. Moreover, the debug feature is separated from the rest of the code.
The code above showed dependency injection done by hand, but there are many dependency injection frameworks that can assist you.
Plugins
If you want to decouple the varying behavior even more, you can consider putting features into separate plugins. A plugin is basically a shared library that is loaded dynamically by the application, after the program has started. At given events, the application can call code from the plugin. To do so, a few pieces need to be put in place:
- The program must define one or more interfaces that the plugins must implement. Otherwise, it is impossible to call the plugin from within the application.
- The plugin must be provided as a shared library. The plugins must of course also implement the interface.
- The program must offer a registration mechanism. There are many ways to do this, from a configuration file to loading class that implement an interface by reflection or based on convention.
Plugins make it easy to swap in and out functionality long after the application has been deployed. If the application’s plugin loading mechanism supports searching for plugins, then it is sufficient to deliver the plugin as a shared library and put it in the expected folder, and then the new functionality will be picked up.
Selective installation
A completely different approach is to determine during installation which components of the application will be installed. This is comparable to the plugin solution, except that it might not be that dynamic. For example, during installation, you might determine whether to install 32 bit or 64 bit versions of the application. You might also selectively install (or not) functionality depending on installation time conditions.
Delivering multiple installers
If you can build one installer, you can also build multiple. Since the installer definition shapes the application to some extent, you can use that to your advantage. For example, you can deliver multiple installers, each having different versions of a component. This makes it possible to deliver a debug and release version, or deliver installers specific for a computer architecture. Or you can use it to deliver free and paid versions of your program.
Conditional compilation
Using conditional compilation, code can be included or excluded in the compilation based on compile-time criteria.
public class MainWindow {
private readonly IPanel[] mPanels;
public MainWindow() {
mPanels = new IPanel[] {
new DocumentPanel()
#if DEBUG
, new DebugPanel()
#endif
};
}
}
From a maintainability view, conditional compilation has the same drawbacks as a normal if
construct. However, it makes it possible to remove code from shared objects, which a regular if
normally does not do.
Project build definition
Almost all software build systems make it possible to work with multiple configurations. This can be a Debug or Release configuration, but it might also be a configuration per computer architecture. This is also a selection mechanism that makes it possible to control the delivered features at compile time.
Using branches in version control
Version control branches can also be used to control the features to be delivered. There can be multiple branches, and switching them means switching between sets of functionality. Although it is largely only useful to the software developer, it can still come in handy if you want to quickly perform some tests.
Using variation control mechanisms
The previous part of the article listed a number of ways in which variation in behavior could be achieved for a program. It is clear that not all mechanisms are always applicable. If you develop a web application, there is no installer and if you develop in Java, there is no conditional compilation.
It is important to weight the pros and cons of each technique. Techniques that act at the runtime of the problem are the most flexible. But they also provide feedback late in time: the only point where they can fail is at runtime. Other techniques act at compilation time. These can give fast feedback, but are less flexible instead.