Your browser may have trouble rendering this page. See supported browsers for more information.

This page shows the source for this entry, with WebCore formatting language tags and attributes highlighted.

Title

API Design: The Road Not Taken

Description

<bq>Unwritten code requires no maintenance and introduces no cognitive load.</bq> As I was working on another part of Quino the other day, I noticed that the oft-discussed registration and configuration methods<fn> 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. <h>The difference between <c>Register</c> and <c>Use</c></h> The return type for <c>Register</c> methods is <c>IServiceRegistrationHandler</c> and the return type for <c>Use</c> methods is <c>IApplication</c> (a descendant), The <i>Register*</i> methods come from the IOC interfaces, while the application builds on top of this infrastructure with higher-level <i>Use*</i> configuration methods. This forces developers to write code in the following way to create and configure an application. <code> public IApplication CreateApplication() { var result = new Application() .UseStandard() .UseOtherComponent(); result. .RegisterSingle<icodehandler,>() .Register<icodepacket,>(); return result; } </code> 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. <h>Developers are the Users of a Framework</h> 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 <i>forever</i>. 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 <i>using it</i> 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. <h>Better Sooner than Later</h> 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 <c>[Obsolete]</c> on <i>something</i>. I am well-aware that this is not a simple or cost-free endeavor. However, I happen to know that there aren't <i>that</i> many users of this API yet, so the damage can be controlled. If I wait, then replacing this API with something better <i>later</i> 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. <h>A Better Way</h> Something like the example below would be nice. <code> return new Application() .UseStandard() .RegisterSingle<icodehandler,>() .UseOtherComponent() .Register<icodepacket,>(); </code> 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 <i>and it would just be there</i>. <h>Attempt #1: Use a Self-referencing Generic Parameter</h> In order to do this, I'd already created an issue in our tracker to parameterize the <c>IServiceRegistrationHandler</c> 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 <a href="https://vimeo.com/154564491" author="Benjamin Hodgson" source="Vimeo">Fun with Generics</a>, which starts off with a technique identical to the one I'd planned to use---and that I'd already used successfully for the <c>IQueryCondition</c> interface.<fn> Let's redefine the <c>IServiceRegistrationHandler</c> interface as shown below, <code> public interface IServiceRegistrationHandler<tself> { TSelf Register<tservice,>() where TService : class where TImplementation : class, TService; // ... } </code> 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, <code> public interface IApplication : IServiceRegistrationHandler<iapplication> { } </code> 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 <c>IServiceRegistrationHandler</c>. This means that a lot of extension methods like the one below <code> public static IServiceRegistrationHandler RegisterCoreServices( [NotNull] this IServiceRegistrationHandler handler) { } </code> will now look like this: <code> public static TSelf RegisterCoreServices<tself>( [NotNull] this IServiceRegistrationHandler<tself> handler) where TSelf : IServiceRegistrationHandler<tself> { } </code> This makes defining such methods more complex (again).<fn> in my attempt at implementing this, Visual Studio indicated 170 errors remaining after I'd already updated a couple of extension methods. <h>Attempt #2: Simple Extension Methods</h> 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 <c>Register</c> method, which uses the <c>IServiceRegistrationHandler</c>, <i>and</i> a <c>Use</c> method, which uses the <c>IApplication</c> Here's an example of the corresponding "Use" method: <code> public static IApplication UseCoreServices( [NotNull] this IApplication application) { if (application == null) { throw new ArgumentNullException("application"); } application .RegisterCoreServices() .RegisterSingle(application.GetServices()) .RegisterSingle(application); return application; } </code> 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 <c>IApplication</c> return type is from the <c>Register*</c> methods defined on the <c>IServiceRegistrationHandler</c> 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<fn>, though, so let's prepend the word <c>Use</c>, as shown below: <code> IApplication UseRegister<tservice,>( [NotNull] this IApplication application) where TService : class where TImplementation : class, TService; { if (application == null) { throw new ArgumentNullException("application"); } application.Register<tservice,>(); return application; } </code> 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 <c>IApplication</c> to <c>IServiceRegistrationHandler</c>, we can rewrite the example from above as: <code> return new Application() .UseStandard() .UseRegisterSingle<icodehandler,>() .UseOtherComponent() .UseRegister<icodepacket,>(); </code> 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.<fn> <hr> <ft>See Encodo’s configuration library for Quino <a href="{app}view_article.php?id=412">Part 1</a>, <a href="{app}view_article.php?id=413">Part 2</a> and <a href="{app}view_article.php?id=414">Part 3</a> as well as API Design: Running and Application <a href="{app}view_article.php?id=422">Part 1</a> and <a href="{app}view_article.php?id=426">Part 2</a> and, finally, <a href="{app}view_article.php?id=412">Starting up an application, in detail</a>.</ft> <ft>The video goes into quite a bit of depth on using generics to extend the type system in the <i>direction</i> 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.</ft> <ft>As detailed in the links in the first footnote, I'd just <i>gotten rid</i> of this kind of generic constraint in the configuration calls because it was so ugly and offered little benefit.</ft> <ft>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: <code> interface IA { IA RegisterSingle<tservice,>(); } interface IB : IA { } static class BExtensions { static IB RegisterSingle<tservice,>(this IB b) { return b; } static IB UseStuff(this IB b) { return b; } } </code> Let's try to call the method from <c>BExtensions</c>: <code> public void Configure(IB b) { b.RegisterSingle<ifoo,>().UseStuff(); } </code> The call to <c>UseStuff</c> cannot be resolved because the return type of the matched <c>RegisterSingle</c> method is the <c>IA</c> of the interface method not the <c>IB</c> of the extension method. There is a solution, but you're not going to like it (I know I don't). <code> public void Configure(IB b) { BExtensions.RegisterSingle<ifoo,>(b).UseStuff(); } </code> 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? <a href="http://www.imdb.com/title/tt0078788/quotes?item=qt0324876" source="IMDb">The horror...the horror.</a></ft> <ft>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.</ft>