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.
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: sequence of BetaFile; aBeta: Boolean); begin if aBeta then begin var lProducts := from p in DataAccess.Linq.GetTable<ByUser_BetaProducts>(...) order by p.DisplayOrder; // loop and print out files end else begin var lProducts := from p in DataAccess.Linq.GetTable<ByUser_LicensedProducts>(...) order by p.DisplayOrder; // loop and print out files end; end;
The code that i’ve abbreviated behind the “//loop and print out files” comment was actually just 5 lines, but it was mostly identical for both beta and final downloads. I still had to duplicate it, because the two LINQ queries, of course, returned sequences of different types. Which is kind of a bummer, if you think about it – these are two queries (actually, Data Tables defined in the DA server that apply quite a bit of complex logic) that return the same kind of data, but due to the way LINQ works, because they are different tables, the resulting objects are of different classes.
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 interface property ProductID: System.Guid read; property Name: System.String read; property ImageName: System.String read; property OptionalMessage: System.String read; property DateExpires: nullable System.DateTime read; end;
that defined all the members from the query i needed access to.
Next, i adjusted the LINQ code just ever so slightly:
method DynamicDownloadList.PrintProductFiles(aFiles: sequence of BetaFile; aBeta: Boolean); begin if aBeta then begin var lProducts := from p in DataAccess.Linq.GetTable<ByUser_BetaProducts>(...) order by p.DisplayOrder select duck<IByUserProducts>(p, DuckTypingMode.Weak); // loop and print out files end else begin var lProducts := from p in DataAccess.Linq.GetTable<ByUser_LicensedProducts>(...) order by p.DisplayOrder select duck<IByUserProducts>(p); // loop and print out files end; end;
The only thing i changed is that at the end of the from clauses i added an extra select operator to change what the LINQ query returns: duck-typing the specialized class returned from DA into the custom IByUserProducts interface i declared.
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!
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 call DateExpires when i know it’s not there, and i’m all good!
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.
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.