Duck Typing in Action
I came across a really awesome use of Duck Typing yesterday, and i wanted to share this with you, since i think Duck Typing is one of those features in Oxygene that are incredibly powerful, but underrated – probably even unknown to many users.
This is actual code that is live on our website, and it’s driving the downloads lists for licensed products and betas.
Both download pages are driven by the same .ascx, which runs a query to get the products a user has access to, and then compares it to the available downloads (passed in as aFiles), to determine what files to show. The code looks something like this:
method DynamicDownloadList.PrintProductFiles(aFiles:sequenceof BetaFile; aBeta: Boolean);beginif aBeta thenbegin var lProducts :=from p in DataAccess.Linq.GetTable(...)orderby p.DisplayOrder;// loop and print out files endelsebegin var lProducts :=from p in DataAccess.Linq.GetTable(...)orderby p.DisplayOrder;// loop and print out files end; end; |
So even though both ByUser_BetaProducts and ByUser_LicensedProducts contain fields like ProductID, Name, ImageName, and so on, it’s not really easy to write code that can be shared and work against either version. (To boot, these classes are auto-generated by LINQ, so it’s not like i can just add a common interface to the ancestry list, either.)
Yesterday i set out to expand the logic for the download display a bit (in particular to improve how the unified “Oxygene” download shows for people who own all three platforms), and i found the duplicate code turning too complex to stay duplicated. Sometime it’s fine to copy/paste 3-5 simple lines of code into two places, but this was getting more sophisticated than i liked sharing and maintaining in two places.
Duck Typing to the rescue!
So what i needed to do was write code that could work on two classes that have no common ancestor and no shared interface, but look “similar”. Exactly what Duck Typing was made for!
First, i went ahead and declared the following interface:
type IByUserProducts =public soft interfaceproperty ProductID: System.Guidread;property Name: System.Stringread;property ImageName: System.Stringread;property OptionalMessage: System.Stringread;property DateExpires:nullable System.DateTimeread;end; |
Next, i adjusted the LINQ code just ever so slightly:
method DynamicDownloadList.PrintProductFiles(aFiles:sequenceof BetaFile; aBeta: Boolean);beginif aBeta thenbegin var lProducts :=from p in DataAccess.Linq.GetTable(...)orderby p.DisplayOrderselect duck(p, DuckTypingMode.Weak);// loop and print out files endelsebegin var lProducts :=from p in DataAccess.Linq.GetTable(...)orderby p.DisplayOrderselect duck(p);// loop and print out files end; end; |
Now, instead of returning sequences of ByUser_BetaProducts and ByUser_LicensedProducts respectively, both queries return the same type: a sequence of IByUserProducts.
This, in turn, means that i could take all the code from “// loop and print out files” and move it into a separate function, called from both branches.
Presto: shared code!
DuckTypingMode.Weak
If you’re eagle-eyed, you might have noticed that i’m passing DuckTypingMode.Weak to the duck() function in the first branch, but not the second. What does that do?
Simply put, i lied when said that the two queries returned the exact same set of fields. In fact, the query for release products returns a few extra fields, including the expiration date (which i need to gray out newer downloads if a subscription expired – in contrast to beta downloads, where the whole product just disappears).
DuckTypingMode.Weak allows the duck typing to IByUserProducts to succeed, even though the underlying class does in fact not fully qualify for the interface, because it’s missing the DateExpires property. Oxygene will instead inject a stub that will throw an exception if DateExpires were to be called.
But that’s fine. All i need to do in my now shared code is check the aBeta flag, and make sure i don’t actually callDateExpires when i know it’s not there, and i’m all good!
Summary
To summarize, Duck Typing is an awesome way in Oxygene to let you write code against classes that have the same (or similar) API, but don’t have a common ancestry or shared interfaces.
Of course, if you have full control over the class hierarchy yourself, the easiest (and cleanest way) to achieve this is to just define an interface and have the classes implement it. But sometimes, as in this example, the actual classes are out of your hand. Duck Typing lets you get around this nicely and safely.
Soft Interfaces
You might have noticed that i used the soft keyword when defining the interface, above. This was actually not fully necessary, as the duck() function will work with any kind of interface.
Soft interfaces approach duck typing from the opposite end: by defining the interface as “soft”, i don’t actually need the duck() call, as soft interfaces carry with them the information that they may automatically be duck-typed too, with a simple cast. As such, a simple select p as IByUserProducts would do to convert the types.
However, soft interfaces use the default (static) duck-typing mode by default, not the weak duck-typing. So this approach would have worked for the Beta branch of my code, which is why i chose to explicitly call duck() in both cases, for clarity. (Yes, i could/should drop the unnecessary soft keyword then, but i left it in to have an excuse to talk about it here ;).
You can read more about Duck Typing and Soft Interfaces on our wiki.