It may be my limited imagination and poor design skills, but whenever I've been presented with requirements like the above, I've been forced to make some sort of compromise. Here's why it's difficult also in OOP:
The UI will be aware of these types and will display each type accordingly.
This is the problematic part. As far as I can tell, there's only two ways to address this requirement:
- Treat all 'sub-types' as a special case. If you do it with OOD, this means that the client (the UI) must know about all available sub-types. It can either attempt downcasts in an ad-hoc manner, or it can leverage the Visitor pattern, but in either case, it violates the Open/Closed Principle, because you can't just add a new 'provider' to the system without modifying the UI code.
- Let all 'sub-types' implement a common interface or inherit from a common base class. This interface (or base class) would then have one (or more) methods that the UI can invoke to render the result. Such a method could be called Render, and it would be technology-specific:
- For desktop applications, it might look like this
void Render(Canvas)
(simplified) - that is, it receives some sort of canvas object, upon which it's then asked to render. - For web applications, it might be a function returning an HTML fragment:
string Render()
(again, simplified).
- For desktop applications, it might look like this
While you can come up with more elaborate schemes, they'd tend to be a combination of the above two alternatives.
Each alternative can be modelled in F# without relying on inheritance.
Special cases
Instead of treating each 'sub-type' as a special case, you can define a Discriminated Union of all the various cases:
open System
type BlogResult = {
Title : string
Summary : string }
type MovieResult = {
Title : string
PosterIcon : Uri }
type SearchResult =
| BlogResult of BlogResult
| MovieResult of MovieResult
This has the same disadvantage as the OOD approach to special casing: you'll need to modify (recompile) the UI if you want to introduce a new sub-type.
On the other hand, Discriminated Unions are built-in to F#, are easy to work with, and even give you compile-time checking (which you could also get with the Visitor pattern).
Common interface
As an alternative to using a common interface, you can use a function. Functions, or rather, closures are equivalent to objects.
So, instead of letting the UI consume an interface that defines a string Render()
method, you can let the UI consume a function with this signature: unit -> string
.
Any 'provider' you want to plug into the system simply needs to return a function with this signature for each search result. That function may very well be a closure.