API Design: The Road Not Taken

Published by Marco on

“Unwritten code requires no maintenance and introduces no cognitive load.”

As I was working on another part of Quino the other day, I noticed that the oft-discussed registration and configuration methods[1] were a bit clunkier than I’d have liked. To whit, the methods that I tended to use together for configuration had different return types and didn’t allow me to freely mix calls fluently.

The difference between Register and Use

The return type for Register methods is IServiceRegistrationHandler and the return type for Use methods is IApplication (a descendant), The Register* methods come from the IOC interfaces, while the application builds on top of this infrastructure with higher-level Use* configuration methods.

This forces developers to write code in the following way to create and configure an application.

public IApplication CreateApplication()
{
  var result =
    new Application()
    .UseStandard()
    .UseOtherComponent();

  result.
    .RegisterSingle<ICodeHandler, CustomCodeHandler>()
    .Register<ICodePacket, FSharpCodePacket>();

  return result;
}

That doesn’t look too bad, though, does it? It doesn’t seem like it would cramp anyone’s style too much, right? Aren’t we being a bit nitpicky here?

That’s exactly why Quino 2.0 was released with this API. However, here we are, months later, and I’ve written a lot more configuration code and it’s really starting to chafe that I have to declare a local variable and sort my method invocations.

So I think it’s worth addressing. Anything that disturbs me as the writer of the framework—that gets in my way or makes me write more code than I’d like—is going to disturb the users of the framework as well.

Whether they’re aware of it or not.

Developers are the Users of a Framework

In the best of worlds, users will complain about your crappy API and make you change it. In the world we’re in, though, they will cheerfully and unquestioningly copy/paste the hell out of whatever examples of usage they find and cement your crappy API into their products forever.

Do not underestimate how quickly calls to your inconvenient API will proliferate. In my experience, programmers really tend to just add a workaround for whatever annoys them instead of asking you to fix the problem at its root. This is a shame. I’d rather they just complained vociferously that the API is crap rather than using it and making me support it side-by-side with a better version for usually feels like an eternity.

Maybe it’s because I very often have control over framework code that I will just not deal with bad patterns or repetitive code. Also I’ve become very accustomed to having a wall of tests at my beck and call when I bound off on another initially risky but in-the-end rewarding refactoring.

If you’re not used to this level of control, then you just deal with awkward APIs or you build a workaround as a band-aid for the symptom rather than going after the root cause.

Better Sooner than Later

So while the code above doesn’t trigger warning bells for most, once I’d written it a dozen times, my fingers were already itching to add [Obsolete] on something.

I am well-aware that this is not a simple or cost-free endeavor. However, I happen to know that there aren’t that many users of this API yet, so the damage can be controlled.

If I wait, then replacing this API with something better later will take a bunch of versions, obsolete warnings, documentation and re-training until the old API is finally eradicated. It’s much better to use your own APIs—if you can—before releasing them into the wild.

Another more subtle reason why the API above poses a problem is that it’s more difficult to discover, to learn. The difference in return types will feel arbitrary to product developers. Code-completion is less helpful than it could be.

It would be much nicer if we could offer an API that helped users discover it at their own pace instead of making them step back and learn new concepts. Ideally, developers of Quino-based applications shouldn’t have to know the subtle difference between the IOC and the application.

A Better Way

Something like the example below would be nice.

return
  new Application()
  .UseStandard()
  .RegisterSingle<ICodeHandler, CustomCodeHandler>()
  .UseOtherComponent()
  .Register<ICodePacket, FSharpCodePacket>();

Right? Not a gigantic change, but if you can imagine how a user would write that code, it’s probably a lot easier and more fluid than writing the first example. In the second example, they would just keep asking code-completion for the next configuration method and it would just be there.

Attempt #1: Use a Self-referencing Generic Parameter

In order to do this, I’d already created an issue in our tracker to parameterize the IServiceRegistrationHandler type in order to be able to pass back the proper return type from registration methods.

I’ll show below what I mean, but I took a crack at it recently because I’d just watched the very interesting video Fun with Generics by Benjamin Hodgson (Vimeo), which starts off with a technique identical to the one I’d planned to use—and that I’d already used successfully for the IQueryCondition interface.[2]

Let’s redefine the IServiceRegistrationHandler interface as shown below,

public interface IServiceRegistrationHandler<TSelf>
{
  TSelf Register<TService, TImplementation>()
      where TService : class
      where TImplementation : class, TService;

  // …
}

Can you see how we pass the type we’d like to return as a generic type parameter? Then the descendants would be defined as,

public interface IApplication : IServiceRegistrationHandler<IApplication>
{
}

In the video, Hodgson notes that the technique has a name in formal notation, “F-bounded quantification” but that a snappier name comes from the C++ world, “curiously recurring template pattern”. I’ve often called it a self-referencing generic parameter, which seems to be a popular search term as well.

This is only the first step, though. The remaining work is to update all usages of the formerly non-parameterized interface IServiceRegistrationHandler. This means that a lot of extension methods like the one below

public static IServiceRegistrationHandler RegisterCoreServices(
  [NotNull] this IServiceRegistrationHandler handler)
{
}

will now look like this:

public static TSelf RegisterCoreServices<TSelf>(
[NotNull] this IServiceRegistrationHandler<TSelf> handler)
  where TSelf : IServiceRegistrationHandler<TSelf>
{
}

This makes defining such methods more complex (again).[3] in my attempt at implementing this, Visual Studio indicated 170 errors remaining after I’d already updated a couple of extension methods.

Attempt #2: Simple Extension Methods

Instead of continuing down this path, we might just want to follow the pattern we established in a few other places, by defining both a Register method, which uses the IServiceRegistrationHandler, and a Use method, which uses the IApplication

Here’s an example of the corresponding “Use” method:

public static IApplication UseCoreServices(
  [NotNull] this IApplication application)
{
  if (application == null) { throw new ArgumentNullException("application"); }

  application
    .RegisterCoreServices()
    .RegisterSingle(application.GetServices())
    .RegisterSingle(application);

  return application;
}

Though the technique involves a bit more boilerplate, it’s easy to write and understand (and reason about) these methods. As mentioned in the initial sentence of this article, the cognitive load is lower than the technique with generic parameters.

The only place where it would be nice to have an IApplication return type is from the Register* methods defined on the IServiceRegistrationHandler itself.

We already decided that self-referential generic constraints would be too messy. Instead, we could define some extension methods that return the correct type. We can’t name the method the same as the one that already exists on the interface[4], though, so let’s prepend the word Use, as shown below:

IApplication UseRegister<TService, TImplementation>(
  [NotNull] this IApplication application)
      where TService : class
      where TImplementation : class, TService;
{
  if (application == null) { throw new ArgumentNullException("application"); }

  application.Register<TService, TImplementation>();

  return application;
}

That’s actually pretty consistent with the other configuration methods. Let’s take it for a spin and see how it feels. Now that we have an alternative way of registering types fluently without “downgrading” the result type from IApplication to IServiceRegistrationHandler, we can rewrite the example from above as:

return
  new Application()
  .UseStandard()
  .UseRegisterSingle<ICodeHandler, CustomCodeHandler>()
  .UseOtherComponent()
  .UseRegister<ICodePacket, FSharpCodePacket>();

Instead of increasing cognitive load by trying to push the C# type system to places it’s not ready to go (yet), we use tiny methods to tweak the API and make it easier for users of our framework to write code correctly.[5]


[1] See Encodo’s configuration library for Quino Part 1, Part 2 and Part 3 as well as API Design: Running and Application Part 1 and Part 2 and, finally, Starting up an application, in detail.
[2] The video goes into quite a bit of depth on using generics to extend the type system in the direction of dependent types. Spoiler alert: he doesn’t make it because the C# type system can’t be abused in this way, but the journey is informative.
[3] As detailed in the links in the first footnote, I’d just gotten rid of this kind of generic constraint in the configuration calls because it was so ugly and offered little benefit.
[4]

If you define an extension method for a descendant type that has the same name as a method of an ancestor interface, the method-resolution algorithm for C# will never use it. Why? Because the directly defined method matches the name and all the types and is a “stronger” match than an extension method.

Perhaps an example is in order:

interface IA 
{
  IA RegisterSingle<TService, TConcrete>();
}

interface IB : IA { }

static class BExtensions
{
  static IB RegisterSingle<TService, TConcrete>(this IB b) { return b; }

  static IB UseStuff(this IB b) { return b; }
}

Let’s try to call the method from BExtensions:

public void Configure(IB b)
{
  b.RegisterSingle<IFoo, Foo>().UseStuff();
}

The call to UseStuff cannot be resolved because the return type of the matched RegisterSingle method is the IA of the interface method not the IB of the extension method. There is a solution, but you’re not going to like it (I know I don’t).

public void Configure(IB b)
{
  BExtensions.RegisterSingle<IFoo, Foo>(b).UseStuff();
}

You have to specify the extension-method class’s name explicitly, which engenders awkward fluent chaining—you’ll have to nest these calls if you have more than one—but the desired method-resolution was obtained.

But at what cost? The horror…the horror. (IMDb)

[5] The final example does not run against Quino 2.2, but will work in an upcoming version of Quino, probably 2.3 or 2.4.