Cheap Interfaces

Sometimes it's hard to see the forest through the trees.

Cheap Interfaces

There are many kinds of architecture: hexagon, onion, clean, screaming, etc. Regardless the architecture, we all try to adhere to a couple of rules:

  • Don't mix business logic with infrastructure.
  • Keep dependencies away from the core.
  • Sometimes those are the same thing
  • Sometimes you break rules so that you can ship

Even though we know the rules, sometimes its hard to know where to draw the lines. At no point in my software development career have I looked at a program I wrote and thought it was perfect. As developers, we're often working with imperfect information, with an imperfect skill set. There's always more to know and more to learn. But we have to write the code anyway, and do the best we can with what he have.

Because of this, it's almost inevitable that at some point in our career, when a prototype goes into production, requirements change, a new feature is requested, etc. our assumptions will come back to bite us.

I've been thinking about this problem a lot lately. How can we put ourselves in a position to succeed when we don't know the direction our application will take in the future? How do we cope with the unknown and still produce an application in a reasonable amount of time?

The Cheap Interface

Functions

I think functions are a great abstraction when you're trying move quickly but maintain some flexibility. Their signatures are what I'm calling "cheap interfaces". In .NET, I'm referring specifically to Func<A, B>s and Action<A>s.

In and ideal world, I think we'd want a named interface. I think that's what most .NET programmers are more likely to expect and understand. My struggle is that naming things is hard. Knowing my interfaces before I've started writing the app is even harder. Functions solve the issue because they're a single signature and nothing more. If I need a piece of code to take a string and give me an A, Func<string, A> requires nothing of me other than that I give it something that takes a string and gives me an A. It gives me options. If, in the future, I also end up needing a Func<A, string>, the implementation of those functions could be methods on the same class. Our caller doesn't care!

For example:

public class Impl<A> {
    
    private readonly IDoSomething doSomething;
    
    public Impl(IDoSomething doSomething) {
        this.doSomething = doSomething;
    }
    
    public A ReturnA(string input) => 
        doSomething.WithString(input);
    
    public string ReturnString(A input) => 
        doSomething.WithA(input);
}


public class Caller<A> {

    private readonly Func<string, A> toA;
    private readonly Func<A, string> toString;
    
    public Caller(
        Func<string, A> toA, 
        Func<A, string> toString) {
        
        this.toA = toA;
        this.toString = toString;
    }
    
    public string DoSomeWork(string input) => 
        toString(toA(input));
}

In this example, Impl<A> could provide both Func<string, A> and Func<A, string> to Caller<A>, but if later we need a different implementation for one of Caller<A>'s Func<>s, we could swap it out with no problem. Caller<A> doesn't care, and doesn't need to change.

It's all about tradeoffs

The downside is that in languages that were not originally designed with first class functions in mind, function signatures can be very verbose, and at times, too abstract for many peoples liking. It may be that the return on investment for the indirection isn't worth it, or even in the negative depending on your team. Also, it might be that Func<string, A> and Func<A, string> would fit very naturally together behind a single interface, and Caller<A>'s two Func dependencies are a distraction or productivity killer for your team. Sometimes solving problems is more about the people than the code.

But even if Funcs aren't the ideal solution for a C# code base, I think they're an easy solution to reducing coupling in an application when we're trying to move fast. The thought being that if we don't have time to figure out a proper interface, a function signature can save us from a coupling nightmare in the future. If we don't have time to go back and refactor, we have a usable abstraction in place. It leaves our options open when we're in a rush. Even if it's not ideal, it takes very little effort to use.

It's not a new idea

Using functions as interfaces isn't anything new. F# programmers already do this kind of thing all the time. Functions, functions everywhere! But for C# code, it does seem out of the ordinary.

At the end of the day, this is the strategy pattern in new clothes. Instead of using an interface and its methods to implement the strategy, we use functions. I presented it a little differently because my immediate goal isn't to handle different strategies so much as it is to hide dependencies and make them easy to swap. That doesn't mean it can't be used for a strategy.

What does it look like?

If we're writing C# code, we have to use a dependency injection container (DI container from here on out), or else the code won't be taken seriously. So I'll use a tiny console app with a DI container as an example. Don't pay much attention to how the DI is set up, I'm not attempting to maximize performance here or present a nice DI pattern, so I'm just using AddTransient everywhere to focus solely on the code and how functions work as interfaces.

Lets say we have a class called Application with a private field, Handler, that handles JSON data and returns some result. Simple, but not completely unlike many applications I seem to write these days.

public class Application {

	private readonly Handler handler;

	public Application(Handler handler) {
		this.handler = handler;
	} 

	public void Run() {
		var myData = @"{ id: 1337, message: ""some randome message i might receive"", funFact: ""this might be cool""}";
		var result = handler.Handle(myData);
		Console.WriteLine($"My data Message length is {result}");
	}
}

Our Handler takes the message, deserializes it, and returns the length of one of the pieces. This is contrived but the logic of the handler isn't very important. It's just a representation of our traditional application logic:

using Newtonsoft.Json;

public class Handler {

	public Handler() {}

	public int Handle(string message) => 
		JsonConvert
			.DeserializeObject<MyData>(message)
			.Message
			.Length;
}

MyData is just a POCO:

public class MyData {

	public int Id { get; set; }

	public string Message { get; set; }

	public string FunFact { get; set; }
}

Now lets bring it all together in the Main method:

class Program {

	static void Main(string[] args) {
		Console.WriteLine("Hello World!");

		var services = new ServiceCollection()
			.AddTransient<Handler>()
			.AddTransient<Application>()
			.BuildServiceProvider();

		var app = services.GetRequiredService<Application>();

		app.Run();
	}
}

This is a ridiculously simple application, but I've found that many applications start off almost as simple as this. Then they grow and grow and grow until they become unmanageable. It doesn't happen all at once. One change here, one change there, until we can't keep all the pieces in our head.

Then we're stuck!

Rewind

I'm no master architect, but there are a couple of things here that we can change to give us more flexibility in the future.

Libraries are not forever

It's really easy to fall into this trap in the .NET world. Much of our stack is curated by Microsoft, and the rest of the pieces are staples that have been around a long time. It's easy to assume there's no need to abstract over these pieces of the puzzle. Their solid and don't change often (well, usually don't).

We've seen recently with Newtonsoft Json that this is a bad assumption. The move to the new System.Text.Json from Newtonsoft Json shows that things can change. The good news is that one tiny change to our code can give us the flexibility to switch our deserializer on demand.

public class Handler {

	private readonly Func<string, MyData> deserialize;

	public Handler(Func<string, MyData> deserialize) {
		this.deserialize = deserialize;
	}

	public int Handle(string message) => 
		this.deserialize(message).Message.Length;
}

Instead of using JsonConvert.DeserializeObject(message) directly, we can hide it behind an interface. Deserialize is just a function from string to another data type, string -> MyData in this case. So we add a field to Handler that is a function from string to MyData.

Now our deserializer is just a "plugin" to the handler. JSON is ubiquitous these days, but it might not always be. After our changes, not only will we not have to worry about our JSON deserializer changing, we won't have to worry about our data language changing either. If in the future we use some other data format - XML, RON, GeoJSON, SuperSON - we can swap it in with ease.

Now we have to change main and set it up with our DI container.

class Program {

	static void Main(string[] args) {
		Console.WriteLine("Hello World!");

		var services = new ServiceCollection()
        	.AddTransient<Func<string, MyData>>(_ => 
            	JsonConvert.DeserializeObject<MyData>)
			.AddTransient<Handler>()
			.AddTransient<Application>()
			.BuildServiceProvider();

		var app = services.GetRequiredService<Application>();

		app.Run();
	}
}

Not too bad. We're getting a little more verbose in our Program.cs and a little more abstract elsewhere. I know a lot of people like to use a library, or set up the DI container to auto register all of the classes and interfaces with the container. Unfortunately, I don't think this strategy would work well with auto registration. I prefer registering explicitly, even if it's significantly more verbose, so adding a little extra code to the Program.cs hasn't ever been an issue for me, but if it is for you, using functions may not work well for you or your team.

I mentioned that with the change, we can swap deserializers with ease. If we wanted to switch to System.Text.Json, all we have to do is update the Program.cs:

class Program {

	static void Main(string[] args) {
		Console.WriteLine("Hello World!");

		var services = new ServiceCollection()
        	.AddTransient<Func<string, MyData>>(_ => 
            	(str) => 
                	JsonSerializer
                    	.Deserialize<MyData>(
                        	str, 
                            new JsonSerializerOptions() { 
                            	PropertyNameCaseInsensitive = true
                            }))
			.AddTransient<Handler>()
			.AddTransient<Application>()
			.BuildServiceProvider();

		var app = services.GetRequiredService<Application>();

		app.Run();
	}
}

System.Text.Json is a little different than Newtonsoft. We need an extra argument of JsonSerializerOptions in the function call. Thankfully, functions are pretty flexible, and with a little more ceremony, we can provide the same API string -> MyData for any classes that need it.

In this sample application, we only had one usage of the deserializer, but in a real application we may have multiple. Without the Func<string, MyData> abstraction, we'd have to find every place in the code we deserialize and make a change. With our "function interface" we only have to change it in once place - Program.cs.

A Strategy

We've abstracted away our dependencies behind the cheap interface Func<string, MyData> - awesome! But what if we do want to leave our application open to using a strategy. Maybe we've worked with this client before and know that one day, they'll say they also need the length of MyData.FunFact when a specific flag is passed to the program. I'm sure this falls under the "you ain't gonna need it" (YAGNI) principle, but this is something I love about functions - they're cheap! You don't have to make a new interface, a new file, etc. They're baked in, and don't cost much to use. So lets refactor our Application class to provide some flexibility with the Handler we use.

Since Handler took in a string and returned the length of a field, it can be replaced by any function that takes a string and returns and int - Func<string, int>. We'll replace Handler in Application with the Func<string, int> signature, which will allow us to change the behavior of Application depending on the implementation of Func<string, int> we give it. This kind of thing might not seem very powerful when all you're working with is a function from string to int, but can become incredibly powerful when coupled with generics.

public class Application {

	private readonly Func<string, int> handler;

	public Application(Func<string, int> handler) {
		this.handler = handler;
	} 

	public void Run() {
		var myData = @"{ id: 1337, message: ""some randome message i might receive"", funFact: ""this might be cool""}";
		var result = handler(myData);
		Console.WriteLine($"My data Message length is {result}");
	}
}

In my Main method, I'm going to keep Handler managed by the DI container, but you don't have to do this. You can "new it up" explicitly if you want.

class Program {

	static void Main(string[] args) {
		Console.WriteLine("Hello World!");

		var services = new ServiceCollection()
			.AddSingleton<Func<string, MyData>>(_ =>
				JsonConvert.DeserializeObject<MyData>)
			.AddTransient<Handler>()
			.AddTransient<Func<string, int>>(sp =>
				sp.GetRequiredService<Handler>().Handle)
			.AddTransient<Application>()
			.BuildServiceProvider();

		var app = services.GetRequiredService<Application>();

		app.Run();
	}
}

Now, when our client comes to us and says, "we want the length of MyData.FunFact when a flag is passed to the app, but MyData.Message length when no flag is present, we're ready to go. All we need are some modification to the Program.cs.

Evaluation

As I said earlier, there are trade-offs here.

One of the downsides to using functions when you're using Microsoft's DI container is that it's not nearly as clean as using an interface or a delegate. While I think "clean" code is entirely subjective, I'm confident that the verbosity of AddTransient<Func<string, int>>(sp =>
sp.GetRequiredService().Handle)
isn't as clean as services.AddTransient<IInterface, Impl>() simply because of the amount of text you have to parse. Also, using functions is not familiar to C# developers. An interface will also be more intuitive and look cleaner to C# developers, regardless of the verbosity.

The benefit is that, for very little code, we get enormous flexibility. I only briefly alluded to it, but coupling functions with generics can provide even more flexibility than what I've shown here.

My hope is that using functions as "cheap interfaces" can help when we're trying to move quickly, even in the face of uncertainty.  That functions can give us confidence that our code can weather the inevitability of change in the future.