You could take the web browser/HTML route. That is, whenever your program comes across some config info it doesn't understand (such as something added in a future version, or removed from a past version), it ignores it, and instead only pays attention to the sections it does understand.
This does come with the requirement that individual options behave consistently across versions. Either that, or your config file will rapidly become convoluted and repetitive.
In your example, V1
doesn't know about A\A4
, so it would read it with the rest of the file, but then simply ignore it. Similarly, V2
doesn't care about A\A2
, so anything in that section is ignored by V2
. V3
reintroduces A\A2
, which was ignored, rather than removed by V2
, so it's still there, ready to be read and used.
The key to this is to never remove or change existing options. You can only ignore existing options or add new ones, unless you want to create a breaking change.
Here's a more in depth example of a fictitious program that uses a config file to remember some window settings. Say your V1 config file looks something like this:
[WindowDimensions]
width=400
height=300
[WindowLocation]
x=100
y=100
So V1 has a size and location for the window when it starts up. Here's a little pseudo code to read that and access the variables:
Dictionary loadConfig() {
String configFile = readFile("config.ini")
Dictionary config = parseIni(configFile) // parses the file into a tree of Dictionaries
config = fillMissingDefaults(config) // check for missing elements
// and fill any with (V1) defaults
Log("Window width: %d, height: %d",
config["WindowDimensions"]["width"],
config["WindowDimensions"]["height"]) // logs "Window width: 400, height: 300"
Log("Window location: (%d, %d)",
config["WindowLocation"]["x"],
config["WindowLocation"]["y"]) // logs "Window location: (100, 100)"
return config
}
Then, fully 3D holographic displays come out, and everyone buys one. V2 is written to support them. Unfortunately, the tech is still new, and windows are always full screen, though the resolution is adjustable. So the V2 config file would look something like this:
[WindowDimensions]
width=400
height=300
depth=200
[WindowLocation]
x=100
y=100
We've added a depth to the WindowDimensions
to account for the third dimension. Note that the WindowLocation
section is still there. V2 doesn't need it (everything is fullscreen), but, since we want to maintain backwards compatibility with V1, it remains there. Now, for V2's config loader:
Dictionary loadConfig() {
String configFile = readFile("config.ini")
Dictionary config = parseIni(configFile) // identical to parseIni() in V1
config = fillMissingDefaults(config) // check for missing elements
// and fill any with (V2) defaults
Log("Window width: %d, height: %d, depth: %d",
config["WindowDimensions"]["width"],
config["WindowDimensions"]["height"],
config["WindowDimensions"]["depth"]) // logs "Window width: 400, height: 300, depth: 200"
return config
}
Note that the config
dictionary returned by parseIni()
still contains WindowLocation
and all it's children, but, since V2 doesn't care about it, it simply doesn't use it. Similarly, V1 can still use this new config file, even though there is now a new element in the config["WindowDimensions"]
child dictionary.
Since config
contains everything from config.ini
, this has the added benefit of V2 saving the WindowLocation
settings despite not knowing about it, simply by writing out the whole dictionary.
Now what if the config file was newly generated by V2 and thus really does have no mention of WindowLocation
? That's what the defaults are for. If V1 one reads a new config file that previously had only been written by V2, then it would use, for example a default location of (300, 300), and would save that into the config file for future V1 use, along with all the V2 stuff it "doesn't" know about.