I got to thinking about the Rediscovering Options post. The volatility analysis portion is an important artifact of my design process. The current version requires reading the whole list to comprehend the results. How could I make it better?
The design process is generally not very clean or binary. Many avenues are explored and abandoned. Even the important criteria change as understanding changes. However, the cleaned-up analysis in the post is tantalizingly reminiscent of a product pricing & feature table. A bit of spatial organization and some colored icons should allow quick intuitive interpretation of results and better recall.
Here’s my pedagogical periodical prototype.
For review, the design options are
- Direct: Using a tool like ConfigurationManager to directly access configuration
- Static: Using a global static utility for accessing configuration. We’ll assume named constant keys.
- Accessor: Using a module-specific type for injecting configuration. Values are returned by methods
- Options: Using a module-specific type for injecting configuration. Values are stored as properties.
|Add a configuration value||Easy. Simply reference the key from desired component||Easy. Add a constant for the new value, and add a call in the relevant component.||Easy-ish. Add a method to the relevant type. Potentially create a new config accessor and add it to the composition root||Easy. Add a property to the relevant type. Potentially create a new config data type and inject it to the composition root|
|Remove a configuration value||Unsafe. Configuration can be referenced from anywhere and so all code is susceptible to change. You must search and test the whole system to ensure the value is no longer used. Any remaining references will throw a runtime error||Unsafe, but easier. Configuration can be referenced from anywhere. You must check and test the every system that uses the configuration helper thoroughly because all code is susceptible to change. The search is aided by tooling. Remaining references will cause a compiler error because we're using named constants as value keys.||Easy, safe. Any missed usage of deleted method will be caught at compile time. Since the accessor is created for a certain component, only that component needs to be retested.||Easy, safe. Any missed usage of deleted method will be caught at compile time. Since the type is created for a certain component, only that component needs to be retested.|
|Change config storage (i.e. database, json, xml)||Must change every module that uses configuration||Only need to change the static configuration helper, but every caller is potentially exposed to changed behavior or errors.||The source can be changed per configuration value independently. Only the changed values need to be tested and redeployed.||The source can be changed per configuration value independently. Only the changed values need to be tested and redeployed. It is likely that you will fetch all configuration values for a given type from the same sources though. Still only affects the one component|
|Pull from multiple config sources. (different values from different stores)||Possible, have must change every component that wants a different store||Possible but awkward. Requires internal lookup of config keys to expected store||Easily defined per value||Easily defined per type and not very hard per value|
|Pull from multiple config sources (same value coming from a prioritized list of sources)||Explosive code duplication||Possible and centralized. Errors can leak to every caller and only show when the code path is executed.||Possible, but not well centralized. Must change every accessor that needs multiple sources (or add another abstraction layer)||Possible and centralized. Any errors happen at bind time. Most likely on application start with the creation of the composition root.|
|Run parallel tests with different configuration values||Depends on framework. Probably not possible or at least non-trivial||Not possible||Possible, requires mocking||Possible, does not require mocking|
|Use same component with different configuration in different parts of the system (i.e. Connect to two databases to transfer data. Access a different data store with same schema for different use cases)||Not possible||Not possible||Possible but not elegant||Possible and easy|
|Enable configuration values per culture/language||Depends on framework, probably not possible. At least requires explosive code changes||Possible, probably need to change every caller||Possible and easy (can always inject the culture). At most need to change every config accessor||Possible and easy. Probably only need to change composition root.|
|White label platform, load config per whitelabel customer allowing shared infrastructure with different behavior and potentionally different storage/resources||Same as above||Significant concurrency issues.||Same as above||Same as above|
|Re-use component directly from a new client (i.e. console app as a quick utility)||configuration needed by component is completely opaque. Have to look at code or run it and see errors to know what values are needed. The new client must reference the same configuration framework and have the same configuration resources (i.e. files)||Same as direct, must also drag the static config utility into the new client. Helper likely exposes all of the possible configuration values from the rest of the system whether relevant or not.||Either implement a new accessor or setup the same resources as the old use case.||Trivial to manually bind configuration or use a different binding framework.|
|Move component to a new system||Same as above||Same as above||Same as above||Same as above|
|Release as library||Completely untenable||Completely untenable||Awkward||The expected pattern|