Software Interfaces in the Wild
What’s so special about interfaces anyway? A quick dive into extracting value from interfaces in C# through a real-world example.
What’s so special about interfaces anyway? A quick dive into extracting value from interfaces in C# through a real-world example.
Scenario
Recently Facundo Gauna and I set out to develop a system that highlights the vast performance and maintainability gaps between effective and ineffective implementations of microservices. While working on the system, we discovered that the data we were consuming from a REST API was also available to us in a format that we could toss into a database or even use in flat files if we wanted to. This was great news for us because it removed the limit for us on how many requests we could make to access the data (aka API rate-limiting). So we implemented a database and moved on to changing our code to use that database.
Initial State
By the time we discovered the raw data, we had already written several calls to the REST API using lines like this:
var result = await _httpClient.GetAsync(requestUri);
There’s nothing inherently wrong with that line. In fact, it’s perfectly fine. But to use the database, we needed to replace every HTTP request with a request to the database. Additionally, I had a curiosity regarding the performance flat files vs a database, so I also wanted to test a third method of data access. Here’s what the initial implementation looked like (notice that our core logic had a hard dependency on the specific class we were using to access the data):
Core
Http Client
This is a sign of poor planning (
guilty!). In order to access the data another way, we needed to replace every call to the Http Client with another way of accessing the data. If we wanted to switch back again later, we would need to do another refactor. Yuck! At this point, I recalled reading Robert Martin’s (aka Uncle Bob) book, Clean Code. In his book, Uncle Bob suggests separating business logic from data access classes such that the business logic/core of an application doesn’t require change when data access classes change or get replaced. In practice, this means that an interface should be passed in place of concrete data access classes. This allows the core of an application to know how to make requests to the classes that will access the data (methods to call, data to send, and data to receive), but know nothing else about the implementation of the class (how it interacts with the data).
To drive the point home, an analogy is that a power company (data access class
), a generator (data access class
), and an inverter (data access class
) all produce power in different ways. Each of them has an outlet (interface
) that an electrical cord (application core
) can plug into regardless of the fact that the implementation (data access class
) is different for each one. They all provide electric in different ways, but the outlet (interface
) still lets the electrical cord (application core
) use each implementation without needing to change or know which one it is using. Here’s what that looks like:
Power Cord (Core)
Socket (Interface)
Generator (Implementation)
Inverter (Implementation)
Power Company (Implementation)
Ultimately, we opted not to do a find and replace for implementing the database, but instead to implement an interface and some data access classes that implemented the interface. This would allow us to swap on the fly or easily try out additional data access layers if we found one that interested us.
Implementing the Interface
To begin implementing the interface, we had to go to each of the places where we wrote HTTP requests and replace those requests with a call to an interface. Behind the scenes, that interface needed to be implemented by concrete classes that would have two primary tasks:
- Make a request to the appropriate datasource (API or database).
- Form and return the objects that our calling methods expected to receive.
For the sake of simplicity, I am going to narrow the focus to a single endpoint that gets the details of a food item.
Step 1: Create an Interface
public interface IDataAccessor
{
Task<GetFoodResult> GetFood(List<int> ids);
}
The method in this interface takes in a list of integers and returns an object containing a list of corresponding food items. That’s all there is to it. Pretty simple.
Step 2: Create the Implementing Classes
We began with the class that would be interacting with the REST API. I have removed the code to show the concept in a more focused manner.
REST API Data Access Class
public class RestDataAccessor : IDataAccessor
{
public async Task<GetFoodResult> GetFood(List<int> ids)
{
// 1. Retrieve data from the API
// 2. Convert the HTTP response to an object
// 3. Return the object
}
}
This gave us:
Interface
RestDataAccessor
Database Data Access Class
public class DbDataAccessor : IDataAccessor
{
public async Task<GetFoodResult> GetFood(List<int> ids)
{
// 1. Retrieve data from the database
// 2. Convert database response to an object
// 3. Return the object
}
}
And now:
Interface
RestDataAccessor
RestDataAccessor
Step 3: Call the Interface
In the places where we were making direct HTTP requests, we now needed to call the interface instead. So this:
var result = await _httpClient.GetAsync(requestUri);
Was altered to look more like this:
var result = await _dataAccessor.GetFoodAsync(ids);
Doesn’t look much different, huh? After this was completed, we were now matching our electrical cord example with this:
Application Core
Interface
RestDataAccessor
RestDataAccessor
Making it Configurable
Our primary goal when we began this task was to produce a solution that would allow us to easily swap out the concrete classes that we created (DbDataAccessor
and RestDataAccessor
). With the work we had done to this point, code changes were still required to choose which data source we wanted to use. This meant that we had to go in to our code and change all lines with new <type>DataAccessor
to new <differentType>DataAccessor
, do a build, and then release. But since any of those classes were going to be implementing the IDataAccessor interface, we were able to leverage dependency injection and app settings in ASP.Net Core.
In the ConfigureServices method of our Startup.cs, we added this:
var datasources = Configuration.GetSection("Datasources");
var datasource = datasources.GetValue<string>("Datasource");
if (datasource == "NdbApi")
{
services.AddHttpClient();
var apiSettings = datasources.GetSection("NdbApi").Get<NdbApiSettings>();
services.AddSingleton(apiSettings);
services.AddSingleton<IDataAccessor, RestDataAccessor>();
}
else if (datasource == "Database")
{
var connectionString = datasources.GetValue<string>("Database:ConnectionString");
services.AddSingleton(connectionString);
services.AddDbContext<DbContext>(options => options.UseSqlServer(connectionString));
services.AddTransient<IDataAccessor, DbDataAccessor>();
}
else
{
throw new Exception("No datasource specified");
}
From a high level, the block above is looking at a field from app settings and determining which data source the application should use. Then, it is adding to the services any of the required dependencies that it will need to inject. The key lines are these two:
services.AddSingleton<IDataAccessor, RestDataAccessor>();
services.AddTransient<IDataAccessor, DbDataAccessor>();
Those two lines specify that we want to add the IDataAccessor interface to the services with a specific implementation. Once that is complete, the class that uses the data accessor (var result = await _dataAccessor.GetFoodAsync(ids);
) no longer needs to do a new <type>DataAccessor()
. Instead, that class can have a constructor that receives the IDataAccessor interface from the service list via dependency injection. That looks like this:
public GetFoodController([FromServices]IDataAccessor dataAccessor)
{
_dataAccessor = dataAccessor;
}
Notice that the class does not care which specific implementation is behind the interface. All it cares about is that it has the interface.
Now, one last piece for configurability. Here’s what our appsettings.json looks like:
}
"Datasources": {
"Datasource": "Database",
"Database": {
"ConnectionString": "<redacted>"
},
"NdbApi": {
"Key": "<redacted>",
"Uri": "<url>"
}
}
}
The most interesting line in there is this one "Datasource": "Database"
because that’s what we are using in our Startup.cs to determine which datasource to use when the application starts. So, if we wanted to suddenly switch to the REST API, we would only need to change "Datasource": "Database"
to "Datasource": "NdbApi"
and restart the application. No rebuilding or redeploying.
Wrap Up
Interfaces are a very powerful tool when used appropriately, but they don’t belong everywhere. For the places where they do make sense, here are a few benefits of their implementation:
- Increased testability (easy mocking)
- Modular code (plug and play)
- Greater degree of configurability
- Core logic is more protected from bugs that can result from refactoring
If you would like to follow the progress of our microservices journey, stay tuned here or follow me on twitter @FundamentalDev. Facundo can be found on Twitter @gaunacode.
The code used for this article can be found here in this repository (Look at the GetFood folder specifically).