Separate Configuration. Here’s Why.
Consider the configuration code in the constructor for a hypothetical ApiClient
, written in C#.NET[1].
public class ApiClient { private string PageSize { get; } private string BaseUrl { get; } public ApiClient() { // Configuration PageSize = ConfigurationManager.AppSettings["ApiPageSize"]; BaseUrl = ConfigurationManager.AppSettings["ApiBaseUrl"]; } // Methods doing the work of ApiClient // and using PageSize & BaseUrl properties. }
The ConfigurationManager.AppSettings
indexed property calls retrieve a value for the specified key from a configuration file.
Only One Responsibility
The part I want to concentrate on is the AppSettings
configuration calls. The fact that a web client class retrieves the configuration settings is unimportant—it could just as well have been a Business Logic class.
The central issue here is the coupling of two responsibilities that are independent of one another and, therefore, ought to be kept separate:
- Responsibility 1: How the client interacts with the remote API, and
- Responsibility 2: How we get our configuration values.
These concerns change for different reasons and do not belong together.
What do we mean by ‘change for different reasons‘?
We are the developers who make changes to this ApiClient
class. What if we decided to store and retrieve configuration values from a database or a custom configuration subsystem? Should a change to the configuration implementation affect the working of the client code? No, of course not. The client code should not care how it is configured; it should only care that it is configured. The inverse also applies: A change to the client code should not affect its configuration.
How do we resolve this issue?
By extracting the reading of configuration data into a separate class.
OK, let’s extract the configuration code from ApiClient
.
First, we create an interface for the configuration values:
public interface IApiClientConfiguration { string PageSize { get; } string BaseUrl { get; } }
Then we implement the interface:
public class ApiClientConfiguration : IApiClientConfiguration { public string PageSize => ConfigurationManager.AppSettings["ApiPageSize"]; public string BaseUrl => ConfigurationManager.AppSettings["ApiBaseUrl"]; }
The calls to ConfigurationManager.AppSettings
that were in ApiClient
are now in ApiClientConfiguration
. Nice.
All we need to do now to configure our ApiClient with an instance of the new ApiClientConfiguration class is to inject our ApiClientConfiguration via the ApiClient constructor:
private IApiClientConfiguration Config { get; } public ApiClient(IApiClientConfiguration config) { Config = config; }
Once assigned to the Config
property, we may retrieve the configuration values directly via Config.PageSize
and Config.BaseUrl
. A more elegant, more concise alternative would be to assign configuration values to read-only forwarding properties:
private string PageSize => Config.PageSize; private string BaseUrl => Config.BaseUrl;
Putting it all together, the ApiClient
becomes:
public class ApiClient { private IApiClientConfiguration Config { get; } private string PageSize => Config.PageSize; private string BaseUrl => Config.BaseUrl ; public ApiClient(IApiClientConfiguration config) { Config = config; } // Methods doing the work of ApiClient // and using PageSize & BaseUrl properties. }
Class ApiClient
no longer contains the configuration implementation detail—we have extracted it into a separate class.
How do we instantiate ApiClient
with our configuration[2]?
Like so:
var config = new ApiClientConfiguration(); var client = new ApiClient(config);
Configuration from the Database
If we wanted to retrieve the configuration values from the database, ApiClient
would not need to be modified. That is a big win. All we would need to do is write a new configuration class implementing the IApiClientConfiguration
interface:
public class ApiClientDatabaseConfiguration : IApiClientConfiguration { public string PageSize => // Get PageSize from database public string BaseUrl => // Get BaseUrl from database }
Consequently, ApiClient
would now be configured via an instance of the ApiClientDatabaseConfiguration
:
var config = new ApiClientDatabaseConfiguration(); var client = new ApiClient(config);
Now we have a much neater separation of concerns. Configuration lives inside ApiClientConfiguration
or ApiClientDatabaseConfiguration
, and the client code is encapsulated inside ApiClient
. Changes in one class do not need to affect the other.
Conclusion
Configuration retrieval is a concern that ought to be independent, and thus separated, from other logic.
Footnotes:
[1] The point that we should separate configuration specifics from other concerns applies to all languages, not just C#.
[2] In .NET, we normally let an Inversion of Control (IoC) framework like Ninject or AutoFac take care of instantiating the desired implementation of IApiClientConfiguration. Here we’ve done it the old-fashioned, manual way.
Leave a Reply
Want to join the discussion?Feel free to contribute!