Metaprogramming, OOP, AOP (Part 2)
Assuming we're still free to modify the existing library classes (I'll get back to this later), we could easily approach the problem as follows:
- identify a root class, e.g. Control, and add an abstract Localize responsibility.
- identify a derived class (let's call it ContainerControl), that is, a control which is mainly used as a container for other controls. Here Localize could provide a default implementation, which is basically to iterate over all the inner controls and forward the Localize call.
- make sure Localize is called at the appropriate time, e.g. before the control is drawn, or when the current language changes. There are several performance issues here, quite interesting in practice, but largely irrelevant to the discussion.
- Localize must have access to some kind of Dictionary class which keeps track of translations for different languages. For sake of simplicity, we could assume Dictionary to be a singleton, so that we don't have to worry about passing parameters around.
In a sense, that could pretty much be it. Each concrete control (e.g. Label, Button, ListView, etc) will implement its own localization logic. Label and Button will get their own text from the dictionary; ListView may forward the call to each ColumnHeader (to keep a design similar to the .NET version). And so on.
There are obviously a few issues with this simple (almost simplistic) design:
- there is no much reuse between controls; Label and Button will implement Localize basically in the same way, in two different places. We could fix this by implementing the common part in Control, but still we're not pushing reuse much further.
- controls have no idea of their context (that is, the meaningful nesting of ContainerControls from their parent up to the main window) during localization. This could be useful to share localization strings and/or to localize strings while considering context. However, this can be fixed in various ways, e.g. by pushing contextual information from a ContainerControl to contained controls, in the Localize call, or by walking up to the parent, and so on.
- there is no separation of concerns. Actually, localization has just become a cross-cutting concern, which is being implemented in many different classes (although all of them belong to the same hierarchy).
- (this is by far the worst problem) if you don't own the type system, and the owner didn't put in the Localize responsibility, you're out of luck, unless your language allows you to add polymorphic behaviour to an existing hierarchy.
Let's play with the last issue for a while. An unfortunate consequence of OO thinking is that it's always hard to consider a class "complete". For instance, your favorite String class may not have that regular-expression-based Split function you love so much. In most cases, you can put that function into another class, usually with a sad name like StringUtilities :-), and live with that. If your language allows extension methods a-la LINQ, you can add methods to existing classes without breaking the natural syntax for method invocation. However, you need true open classes if you want to add polymorphic behaviour to an existing hierarchy. Extension methods won't cut it. Mixin, or mixin layers, could be used to some advantage.
Having said that, is there an OO way to deal with all the problems above, while staying within the realm of a "restrictive" language, without open classes, and without having to change the library source code?
Turns out you can push some responsibility outside the control and inside a newborn class. In fact, if you look at the implementation of your average control, you might find that it has to keep the unlocalized text somewhere (e.g. in a string that you can also set at design time). That string is then passed to the dictionary as a key at run-time (possibly with some context information) to get a translation in the current language. Now, if you've internalized OO thinking, you'll recognize that the control is managing the strings. That's because the string doesn't know any better. Now, suppose you change all instances of string inside controls (all those which requires localization anyway) with a smarter string, let's say LocalizedString. A LocalizedString would store the unlocalized version but return the localized version when you read it. It will take care of Dictionary lookup, caching, watching for changes in the current language, and so on, all in a single place. No more duplications. Better separation of concerns. The programmer will declaratively say (by using string or LocalizedString) if he/she needs a language-independent or language-dependent string. That would be it.
Again, there are a few issues with this design:
- it's quite harder for the string to find out about its context. While controls can usually navigate to their parent (another Control), it would be quite hard for a string to navigate to whatever object is storing the string. While a Control may have a Localize method which takes parameter, our LocalizedString should be able to return the localized text through a parameterless read (ideally, a conversion operator, so that we can blissfully ignore the whole localization issue).
We could approach this problem from a few angles, and for a simple problem like string localization, we could even succeed. Unfortunately, the technique doesn't work so well for other problems, like persistence. You may resort to reflection, but I'll get to this later.
- just like before, if you don't own the code (or can afford to change it anyway) you're in big troubles: adopting this design is a pervasive change inside the whole Control hierarchy. What is worse, open classes won't help you a bit.
What we're witnessing is a major issue with OOP. Thinking in objects is like creating a foundation. If you get it wrong, you're done. If it's your own code, this might not be a big deal - refactoring is always possible (not necessarily cheap, but possible). But if you're dealing with third-party code, you often suffer from an impedance mismatch between their design and your needs. I do not consider open source as a cure - even if (e.g.) the .NET framework was open source, I would not venture in any massive change, because that's maintenance hell.
Given this major issue with OOP, I find it rather myopic that some features, like open classes and structural conformance to interfaces, are not more widely adopted by language designers. For languages like Java and C#, it's also rather myopic that you can't declaratively say that whenever an object of type T1 is requested, an object of type T2 (usually derived from T1) must be created instead. Not building these features in an OOP language means ignoring real problems, which could be solved without compromising type safety or efficiency. There are also real-world languages (like Objective C) which have shown how to deal with some of these issues in practice. Too bad we, as a community, don't seem to learn enough from the past :-)
Back to our problem: so far, we tried to allocate behaviour inside the existing principal decomposition, or to push it inside a finer-grained class (which should then be used pervasively inside the principal decomposition). Is there a third approach? I didn't play the interface card yet, but that seems quite at odd with the problem. Coming up with a useful ILocalizable (bleah :-) interface, which does not require pervasive, cross-cutting changes inside the principal decomposition to be implemented, seems quite hard to say the least.
There is, of course, the reflective/introspective card yet to play. However, this brings us so close to the AOP way of thinking, that I'll discuss this option while looking at my toy problem from the AOP perspective.