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: To Generic or not Generic? (Part II)

Description

<img attachment="cupt.jpg" align="right">In this article, I'm going to continue the discussion started in <a href="{app}view_article.php?id=3165">Part I</a>, where we laid some groundwork about the state machine that is the startup/execution/shutdown feature of Quino. As we discussed, this part of the API still suffers from <iq>several places where generic TApplication parameters [are] cluttering the API</iq>. In this article, we'll take a closer look at different design approaches to this concrete example---and see how we decided whether to use generic type parameters. <h>Consistency through Patterns and API</h> Any decision you take with a non-trivial API is going to involve several stakeholders and aspects. It's often not easy to decide which path is best for your stakeholders and your product. <pullquote align="left" width="280px">For any API you design, consider how others are likely to extend it---and whether your pattern is likely to deteriorate from neglect.</pullquote>For any API you design, consider how others are likely to extend it---and whether your pattern is likely to deteriorate from neglect. Even a very clever solution has to be balanced with simplicity and elegance if it is to have a hope in hell of being used and standing the test of time. In Quino 2.0, the focus has been on ruthlessly eradicating properties on the <c>IApplication</c> interface as well as getting rid of the descendant interfaces, <c>ICoreApplication</c> and <c>IMetaApplication</c>. Because Quino now uses a pattern of placing sub-objects in the IOC associated with an <c>IApplication</c>, there is far less need for a generic <c>TApplication</c> parameter in the rest of the framework. See <a href="{app}view_article.php?id=412">Encodo’s configuration library for Quino: part I</a> for more information and examples. This focus raised an API-design question: if we no longer want descendant interfaces, should we eliminate parameters generic in that interface? Or should we continue to support generic parameters for applications so that the caller will always get back the type of application that was passed in? Before getting too far into the weeds<fn>, let's look at a few concrete examples to illustrate the issue. <h>Do Fluent APIs require generic return-parameters?</h> As discussed in <a href="{app}view_article.php?id=414">Encodo’s configuration library for Quino: part III</a> in detail, Quino applications are configured with the "Use*" pattern, where the caller includes functionality in an application by calling methods like <c>UseRemoteServer()</c> or <c>UseCommandLine()</c>. The latest version of this API pattern in Quino recommends returning the application that was passed in to allow chaining and fluent configuration. For example, the following code chains the aforementioned methods together without creating a local variable or other clutter. <code> return new CodeGeneratorApplication().UseRemoteServer().UseCommandLine(); </code> What should the return type of such standard configuration operations be? Taking a method above as an example, it could be defined as follows: <code> public static IApplication UseCommandLine(this IApplication application, string[] args) { ... } </code> This seems like it would work fine, but the original type of the application that was passed in is lost, which is not exactly in keeping with the fluent style. In order to maintain the type, we could define the method as follows: <code> public static TApplication UseCommandLine<tapplication>(this TApplication application, string[] args) where TApplication : IApplication { ... } </code> This style is not as succinct but has the advantage that the caller loses no type information. On the other hand, it's more work to define methods in this way and there is a strong likelihood that many such methods will simply be written in the style in the first example. <pullquote width="250px" align="right">Generics definitely offer advantages, but it remains to be seen how much those advantages are worth.</pullquote>Why would other coders do that? Because it's easier to write code without generics, and because the stronger result type is not needed in 99% of the cases. If every configuration method expects and returns an <c>IApplication</c>, then the stronger type will never come into play. If the compiler isn't going to complain, you can expect a higher rate of entropy in your API right out of the gate. One way the more-derived type would come in handy is if the caller wanted to define the application-creation method with their own type as a result, as shown below: <code> private static CodeGeneratorApplication CreateApplication() { return new CodeGeneratorApplication().UseRemoteServer().UseCommandLine(); } </code> If the library methods expect and return <c>IApplication</c> values, the result of <c>UseCommandLine()</c> will be <c>IApplication</c> and requires a cast to be used as defined above. If the library methods are defined generic in <c>TApplication</c>, then everything works as written above. This is definitely an advantage, in that the user gets the <i>exact</i> type back that they created. Generics definitely offer advantages, but it remains to be seen how much those advantages are worth.<fn> <h>Another example: The <c>IApplicationManager</c></h> Before we examine the pros and cons further, let's look at another example. In Quino 1.x, applications were created directly by the client program and passed into the framework. In Quino 2.x, the <c>IApplicationManager</c> is responsible for creating and executing applications. A caller passes in two functions: one to create an application and another to execute an application. A standard application startup looks like this: <code>new ApplicationManager().Run(CreateApplication, RunApplication);<fn></code> <pullquote width="290px" align="right">Generic types can trigger an <i>avalanche of generic parameters</i>(tm) throughout your code.</pullquote>The question is: what should the types of the two function parameters be? Does <c>CreateApplication</c> return an <c>IApplication</c> or a caller-specific derived type? What is the type of the application parameter passed to <c>RunApplication</c>? Also <c>IApplication</c>? Or the more derived type returned by <c>CreateApplication</c>? As with the previous example, if the <c>IApplicationManager</c> is to return a derived type, then it must be generic in <c>TApplication</c> and both function parameters will be generically typed as well. These generic types will trigger an <i>avalanche of generic parameters</i>(tm) throughout the other extension methods, interfaces and classes involved in initializing and executing applications. That sounds horrible. This sounds like a pretty easy decision. Why are we even considering the alternative? Well, because it can be very advantageous if the application can declare <c>RunApplication</c> with a strictly typed signature, as shown below. <code>private static void RunApplication(CodeGeneratorApplication application) { ... }</code> Neat, right? I've got my very own type back. <h>Where Generics Goes off the Rails</h> However, if the <c>IApplicationManager</c> is to call this function, then the signature of <c>CreateAndStartUp()</c> and <c>Run()</c> have to be generic, as shown below. <code> TApplication CreateAndStartUp<tapplication>( Func<iapplicationcreationsettings,> createApplication ) where TApplication : IApplication; IApplicationExecutionTranscript Run<tapplication>( Func<iapplicationcreationsettings,> createApplication, Action<tapplication> run ) where TApplication : IApplication; </code> These are quite messy---and kinda scary---signatures.<fn> if these core methods are already so complex, any other methods involved in startup and execution would have to be equally complex---including helper methods created by calling applications.<fn> The advantage here is that the caller will always get back the type of application that was created. The compiler guarantees it. The caller is not obliged to cast an <c>IApplication</c> back up to the original type. The disadvantage is that all of the library code is infected by a generic <tapplication> parameter with its attendant <c>IApplication</c> generic constraint.<fn> <h>Don't add Support for Conflicting Patterns</h> The title of this section seems pretty self-explanatory, but we as designers must remain vigilant against the siren call of what seems like a really elegant and strictly typed solution. <pullquote width="280px" align="left">But aren't properties on an application exactly what we just worked so hard to eliminate?</pullquote>The generics above establish a pattern that must be adhered to by subsequent extenders and implementors. And to what end? So that a caller can attach properties to an application and access those in a statically typed manner, i.e. without casting? But aren't properties on an application exactly what we just worked so hard to eliminate? Isn't the recommended pattern to create a "settings" object and add it to the IOC instead? That is, as of Quino 2.0, you get an <c>IApplication</c> and obtain the desired settings from its IOC. Technically, the cast is still taking place in the IOC somewhere, but that seems somehow less bad than a direct cast. If the framework recommends that users <i>don't</i> add properties to an application---and ruthlessly eliminated all standard properties and descendants---then why would the framework turn around and add support---at considerable cost in maintenance and readability and extendibility---for callers that expect a certain type of application? <h>Wrapping up</h> Let's take a look at the non-generic implementation and see what we lose or gain. The final version of the <c>IApplicationManager</c> API is shown below, which properly balances the concerns of all stakeholders and hopefully will stand the test of time (or at least last until the next major revision). <code> IApplication CreateAndStartUp( Func<iapplicationcreationsettings,> createApplication ); IApplicationExecutionTranscript Run( Func<iapplicationcreationsettings,> createApplication, Action<iapplication> run ); </code> These are the hard questions of API design: ensuring consistency, enforcing intent and balancing simplicity and cleanliness of code with expressiveness. <hr> <ft>A predilection of mine, I'll admit, especially when writing about a topic about which I've thought quite a lot. In those cases, the instinct to just skip "the object" and move on to the esoteric details that stand in the way of an elegant, <i>perfect</i> solution, is very, very strong.</ft> <ft>This more-realized typing was so attractive that we used it in many places in Quino without properly weighing the consequences. This article is the result of reconsidering that decision.</ft> <ft>This call looks the same for all UI (console, Winform, WPF, etc.), all services (e.g. ASP.NET, Windows-services, etc.) as well as for automated tests. This fact isn't germane to the discussion above, but it's pretty neat in its own right. All an application has to do is define two methods with the right signatures and call the appropriate <c>Run()</c> method for the desired type of application. Almost all of the startup code is shared and the pattern is the same everywhere.</ft> <ft>Yes, the C# compiler will allow you to elide generics for most method calls (so long as the compiler can determine the types of the parameters without it). However, generics cannot be removed from constructor calls. These must always specify all generic parameters, which makes for messier-looking, lengthy code in the caller e.g. when creating the <c>ApplicationManager</c> were it to have been defined with generic parameters. Yet another thing to consider when choosing how to define you API.</ft> <ft>As already mentioned elsewhere (but it bears repeating): callers can, of course, eschew the generic types and use <c>IApplication</c> everywhere---and most probably will, <i>because the advantage offered by making everything generic is vanishingly small.</i>. If your API looks this scary, entropy will eat it alive before the end of the week, to say nothing of its surviving to the next major version.</ft> <ft>A more subtle issue that arises is if you <i>do</i> end up---even accidentally---mixing generic and non-generic calls (i.e. using <c>IApplication</c> as the extended parameter in some cases and <c>TApplication</c> in others). This issue is in how the application object is registered in the IOC. During development, when the framework was still using generics everywhere (or almost everywhere), some parts of the code were retrieving a reference to the application using the most-derived type whereas the application had been registered in the container as a singleton using <c>IApplication</c>. The call to retrieve the most derived type returned a <i>new instance</i> of the application rather than the pre-registered singleton, which was a subtle and difficult bug to track down.</ft>