Configuration Framework
Versatile configuration capabilities with adjustable level of detail is an indispensable mainstay of any software system that aims to meet generally mandated architecture and design principles.
Some non exhaustive examples should help to illustrate this even more
Open-Close principle
A piece of code (like a function or method) that is operating on the data contained in a file could be made much more useful if the name of the file would be passed as a parameter instead of having the file being hard-coded. This is pretty obvious since it avoids the need to change and recompile the program every time the name of the file would change.
(So the principle says, the function is open for extension, but closed for modification…)Separation of Concern
By separating the definition and storage of parameters from the procedures and algorithms that are controlled and fine tuned by these parameters.Dependency Injection
In order to have loosely coupled application services we would follow all sorts of design pattern like dependency inversion and explicit dependency declaration - but at the point where the rubber meets the road, the big question is: Who finally defines which implementation of an interface is effectively injected into the service?
Yes right, the configuration of course!Architectural Agility
Provides deployment and customization flexibility in any case where we would need to deploy an application into different environments like local development, integration testing, cloud containers or just different customers with even different requirements - we could avoid to create specifically targeted builds by just the adjustment of the configuration.
Now that we have gained some motivation for the demand of a flexible configuration, lets see how it can be supported by the Tlabs Library.
Configuration Source
The startup helpers of the Tlabs Library can arrange the application's configuration to be sourced as an aggregate of any location like:
Command line parameters
Environment variables
one ore more JSON files
custom extension
(e.g. from a centralized configuration store)
The resulting configuration contents actually end up in an source agnostic hierarchical key/value representation to be consumed programmatically by any component or service of the application. But more typically these are specifically used by the Tlabs Library readily provided configuration framework to setup all application services.
Configuration Targets
In general all framework and application services that are aimed to be
configured are in turn managed by a small set of core or root
services:
(with the popular
DDD approach
these are also referred to as shared kernels)
Application Host
(A root application service to mange all core lifetime functionality which could include optional HTTP or similar network protocol or system related low level background services.)Application Service Provider
(A container, service factory, dependency resolver and lifecycle manager for all configured application services.)App Middleware (optional)
(A pipeline (building a chain of responsibility) to coordinate data flow between the host layer, through optional selectable intermediate middleware filters and the effective application services.)
Technically these kernel services are being setup by application of the builder pattern in order to assemble a complex object by incrementally adding properties and details until the final service object is ready to be actually build or constructed. (The resulting complex service would now serve as a container for a large and complex set of information and functionality.)
Motivated by the reasons outlined at the beginning of this document the ambition of the Tlabs Library is to help making the usual programmatically hard-coded build and setup process for the core building blocks of an application much more configurable as it normally would be out of the box.
This is achieved with the introduction of Configurators used to incapsulate small pieces of the entire configuration. A Configurator takes the builder of the target root service and uses it to programmatically apply the configuration of one aspect (or even several closely related ones) of the global setup. (Please refer to this typical Configurator as an example).
The configuration itself now specifies which Configurators are getting applied (or more technical: are getting executed) and also which optional parameters these Configurators will take.
A minimal example of the configuration of the Application Service Provider would now look like this:
"applicationServices": {
"systemCli": {
"type": "Tlabs.Sys.SystemCli+Configurator, Tlabs.Core",
"sysCommands": {
"LINUX": {
"shell": ["/bin/bash", "{0}"],
"cmdLines": {
"hello": {"cmd": ["./hello.sh", "{0}", "{1}"], "wrkDir": "rsc/cmd"},
}
}
}
},
//more services...
}
This configures an example service used to run sub-processes with a
command line given by the configuration. Here the Configurator
systemCli
(the name is by one's choice) is specified with its type
(given by its
AssemblyQualifiedName) -
this specific Configurator takes parameters from the configuration
section sysCommands
where any further details for the sub-process
execution are given.
While this is showing how to configure the set of application services,
the same pattern even applies to every kernel service area like
Application Host, Middleware and even Logging and thus allows to
configure the entirety of an application which also implies another
noteworthy configuration option:
If one would stick with the principle to use the Service Provider not
just for dependency injection but also as an abstract factory, the
configuration could now be used to control which particular
implementation of an interface is actually being provided (as the
dependency). Let’s see what this could mean:
While a closer look on the Tlabs Library’s Data Persistence Abstraction is given in a separat document, lets assume here an application that uses a database for storing data and is using a configuration to specify details about the database:
"applicationServices": {
"Persistence.SqLiteDbProvider": {
"type": "Tlabs.Data.Store.SqliteConfigurator, App.SqLiteDataStore",
"config": {
"connection": "DataSource=rsc/store/application_db.sqlite;Foreign Keys=True"
}
},
//more services...
}
This would configure a Sqlite database system
to be used for storage and would e.g. give the option to change the name
of the file containing the database…
What if we were now about to get that same application deployed into a
target environment with the requirement to store all the data in a
PostgreSql db? Well, just let us adjust
the configuration:
"applicationServices": {
"Persistence.PostgreSQL": {
"type": "Tlabs.Data.Store.PostgreSqlConfigurator, App.PostgresDataStore",
"config": {
"connection": "Host=localhost;Database=myAppDb;Username=postgres;Timeout=5"
}
},
//more services...
}
Since by the help of the Tlabs Library our application is designed with complete persistence abstraction in place, the application would simply go on working and now storing/accessing its data from a PostgreSQL database with no need to change a single line of program code or rebuild. Adjusting the configuration is all what it takes (and what makes using a configuration such a big deal)!