Mediators and Inheritance - Maximizing code reuse, Minimizing hacky casting
The query: How does one extend a view/mediator pair in parallel without resorting to registration-time casting or worse, code copy-pasta?
Allow the following illustration.
In a certain museum-codebase, there is a view class, Artwork , and its mediator class, MuseumCurator.
public class Artwork extends Sprite
...
public class MuseumCurator extends Mediator {
[Inject]
public var artwork:Artwork;
...
They are connected in the context by a classic expression.
mediatorMap.mapView(Artwork, MuseumCurator);
The artwork just looks pretty, and the MuseumCurator deals with adjusting the painting to look level on the wall whenever the building tilts due to an earthquake. Things work fine.
A couple of years later, photography becomes big, and the museum starts offering a specialized type of Artwork, the FancyPhoto, a view class that extends Artwork.
public class FancyPhoto extends Artwork
To examine the FancyPhotos for signs of guerrilla photoshopping and steganographic dirty jokes, some of the mediator-staff are supposed to be trained up in the arts of photo-analysis (though of course they're expected to continue their regular duties as MuseumCurators).
public class PhotoanalystMuseumCurator extends MuseumCurator {
[Inject]
public var fancyPhoto:FancyPhoto;
Now, here's where the metaphor and the connections start breaking down. We want the PhotoanalystMuseumCurator to function both as a regular curator for the artwork (photo and regular alike) and in its enhanced functions for dealing with fancy photos. The fancy photos still can be tilted after earthquakes by the same base functionality in MuseumCurator. However, trying to do the following mapping fails at runtime because the injector doesn't understand that the FancyPhoto is still an Artwork and can be injected as such.
mediatorMap.mapView(FancyPhoto, PhotoanalystMuseumCurator); // Runtime error trying to inject into PhotoanalystMuseumCurator.artwork property
The only solution I've found so far is to use the injectViewAs parameter of MediatorMap.mapView to treat the FancyPhoto as a plain old Artwork, and then blindly upcast it to its true state during the mediator registration process.
mediatorMap.mapView(FancyPhoto, PhotoanalystMuseumCurator, Artwork);
...
public class PhotoanalystMuseumCurator extends MuseumCurator {
protected var fancyPhoto:FancyPhoto; // Notably not injected
override public function onRegister():void {
fancyPhoto = artwork as FancyPhoto;
}
...
Bleh, casting is no fun. However, with its current available information passed in through mapView, the mediatorMap can't do a whole lot better (unless I'm missing some critical pattern, please tell me I'm missing something). I don't want to try to expect too much of the automatic-mediator-injector code, but what could we do to make view/mediator inheritance easier?
Here are some ideas.
1) Really bad idea. Automatically try to inject a view class into a mediator as all of its parent-classes. Lots of extra processing, likely to cause conflict with other injections, and generally inelegant.
mediatorMap.mapView(viewClassOrName:*, mediatorClass:Class, injectViewAs:Class = null, autoCreate:Boolean = true, autoRemove:Boolean = true, autoInjectViewAsParentalClasses:Boolean = false)
...
mediatorMap.mapView(FancyPhoto, PhotoanalystMuseumCurator);
2) Bad idea. Automatically try to inject a view class into a mediator as all of its parent classes for an input number of inheritance extensions. Still lots of processing, but slightly less likely to inject your Artwork view class as a Sprite, DisplayObjectContainer, InteractiveObject, DisplayObject, EventDispatcher, AND plain Object.
mediatorMap.mapView(viewClassOrName:*, mediatorClass:Class, injectViewAs:Class = null, autoCreate:Boolean = true, autoRemove:Boolean = true, autoInjectViewAsParentalClasses:Boolean = false, parentalClassInspectionLimit:int = 1)
...
mediatorMap.mapView(FancyPhoto, PhotoanalystMuseumCurator, null, true, true, 1);
3) Okay idea. Modify the injectViewAs parameter to accept an Array of Class objects (or just one Class object). The mediatorMap tries to inject the class as each of the specified Class types. A much more controlled solution, though with slightly more processing than before and a less-clear method signature.
mediatorMap.mapView(viewClassOrName:*, mediatorClass:Class, injectViewAs:* = null, autoCreate:Boolean = true, autoRemove:Boolean = true)
...
mediatorMap.mapView(FancyPhoto, PhotoanalystMuseumCurator, [FancyPhoto, Artwork]);
Is solution #3 worth a shot? Worth changing the IMediatorMap.mapView signature for? Have I overlooked some easy way of extending views and mediators in parallel?
Comments are currently closed for this discussion. You can start a new one.
Keyboard shortcuts
Generic
? | Show this help |
---|---|
ESC | Blurs the current field |
Comment Form
r | Focus the comment reply box |
---|---|
^ + ↩ | Submit the comment |
You can use Command ⌘
instead of Control ^
on Mac
Support Staff 1 Posted by Shaun Smith on 04 Sep, 2010 11:55 AM
Awesome example! Yeh, solutions 1 & 2 are probably not going in the right direction. Solution 3, however, looks very do-able. Something we talked about ages ago, and were planning on implementing, was kind of a half-way solution (though I might prefer option 3 to this now that I think about it):
When you use "injectViewAs" it maps both the "viewClassOrName" and "injectViewAs" for injection. This would be useful in the general case, and doesn't require a signature change for mapView, but isn't as flexible as your array idea.
2 Posted by ZackPierce on 04 Sep, 2010 11:09 PM
So, I went ahead and implemented Solution 3 in a commit to my github fork of the robotlegs framework. I also added some unit tests for mapView's injectViewAs parameter, just to be safe.
If you don't mind a follow-up question, the collaboration guide wasn't clear on the reigning git-etiquette here -- should I toss out a pull request, post to the dev discussion list, or what have you?
Support Staff 3 Posted by Shaun Smith on 08 Sep, 2010 10:58 PM
Nice, thanks! Sorry for the delay.. I've got to do some cleaning up in my dev branch before I can merge this in. As far as dev etiquette goes: for small changes a pull request on GitHub is cool. For API/behavior changes it's probably best to bring it up on the mailing list. Your patch is half-way between the two: it changes the API and behavior, but in a backwards compatible way :)
4 Posted by glidias on 09 Sep, 2010 08:06 AM
I also find a feature like this could allow for a mediator to easily map it's view to multiple interface signatures.
Regarding RL, why not use a dedicated meta tag like [View] for view injections instead? That way the mediator defines the view dependencies explicitly, rather than having to rely on the outside context mappings and increasing chances of wrong mappings being done externally. In this way, only 1 class needs to change (ie. the mediator class itself) rather than also forcing the outside context wirings to change as a result of mediator-to-view api/ui changes. After all, shouldn't the mediator be solely concerned in defining it's view component requirements. Why should the outside application context be concerned over the trivial UI requirements of a particular mediator? Isn't that the mediator's job, alone?
That aside, extending views/mediators may sometimes be very restrictive and sometimes duplicating code may actually be better for future variations.
5 Posted by ZackPierce on 09 Sep, 2010 02:59 PM
Yes, mapping a view to multiple interface injection points is a secondary benefit to this modification to MediatorMap.mapView.
It would certainly make it easier to compartmentalize mediator functionality regarding a single interface into a single helper. From there, a multiple-interface-supporting mediator could use composition to pull in several single-interface-mediation-helpers.
But I'm getting ahead of the game. The proposed [View] metadata tag, while interesting, is also a little beyond the scope of the focused improvement I was hoping to provide and deserves a conversation of its own.
6 Posted by glidias on 09 Sep, 2010 04:07 PM
Instead of using so many generic interface signatures, it's possible to use 1 uniquely identifying interface which extends multiple interfaces. (interfaces support multiple mix-ins). So, you could do something like:
IExpensiveChalkboardArtwork extends ITiltable, IStealable, ITamperable
// not exactly sure about the above syntax, but the above is possible
This way, things are more specific, and the mediator is only coupled with it's view component through 1 clearly defined (self-documenting) interface signature that borrows whatever it needs on the view.swf end, and the application.swf end merely follows suit. Performance-wise, there's less iteration to process since only a single interface class clearly documents all the requirements associated with it.
To have to re-perform so many trivial UI mappings between Context -> (Mediator) ->View, is something I'd rather avoid doing (even on the mediator end..).. I'd rather have such details handled immediately between Mediator/View code, without involving the Context again when api changes occur.
That aside, you get compile-time checking benefits for both the matching 'view.swf' end and 'mediator.swf' end, which are 1-way coupled and compiles that "connecting" interface. This single unique interface for each class acts kinda like a unique "header .h" file documenting the requirements/api for both ends of the application.
However, the above suggestion tends to encourage a specific 1 view class to 1 mediator approach (great for compile-time strict-typing, which encourages use, but not reuse), which doesn't benefit much if you're mapping multiple arbitrary view classes to a particular mediator, and this mediator requires multiple interface signatures for it's view component. I guess writing some code to manually cast stuff in setViewComponent can still be done through a
protected function asCast(asClass:Class):*
method that throws an error if the cast fails. After all, since the view component is arbitrary in such a case, reasonably such checks can be done for such generic (serve-all) mediators. If not, there's always solution 3 as found in: http://github.com/ZackPierce/robotlegs-framework/commit/d77d841d1a7...7 Posted by ZackPierce on 20 Sep, 2010 04:23 PM
Thank you for resolving this by pulling in the "Solution 3" patch. I'm off to simplify my mediator inheritance system without worrying about deviating too far from the core!
Support Staff 8 Posted by Shaun Smith on 20 Sep, 2010 04:34 PM
Cool, no probs! Just remember: "mediators != decorators" ;)
9 Posted by rickcr on 26 Oct, 2010 10:29 PM
I was going to post the following question to the list, but since it's so closely related to this post, I'll reiterate it here. Basically what is the current best practice to handle this kind of inheritance? I'm using RL 1.3.0.
Here was the question I was going to post. Possibly there are some work arounds without using inheritance (Mixins concept?)
I'm having difficulty understanding the best practice here and how you solve issues of keeping things pretty DRY in this use case...
Imagine you have 3 types of views that are all somewhat similar, yet unique as well. For sake of discussion: Dog, Cat, Fish are all views that are of type Animal.
At certain times a system event might be dispatched that only a particular type of animal should respond to. Let's say it's a zoo of animals, and you sound a notification that it's time for the Dogs go eat. In this case I'd figure all the animals will be listening for a "Go Eat" event. When the system event "Go Eat" is dispatched it passes along the animalTypeID in the event so each type of animal could check their id in the event handler to be sure the event notification was really went meant for them.
Since all the animals need to listen for 'Go Eat', it makes sense to me to have that registered in a super class Animal Mediator (imagine the handling of eating is the same for the animal types also and could reside in a generic Animal view that all the animals extend as well.) The problem is each animal type also needs to have its own Mediator to handle unique things, so I'm not sure how to best addressed this within RL.
I've been googling on inheritance within RobotLegs but I'm still really confused on the best practice? or maybe there is a simple way to avoid it (with composition somehow?)
To frame the discussion:
DogView extends AnimalView
CatView extends AnimalView
DogView has DogMediator
CatView has CatMediator
DogMediator and CatMediator should extend AnimalMediator?
I would think this would come up quite often where there is some similar behavior within several types of views that could be handled by an inherited/shared Mediator but I can't really find too much information [other than this support entry.]
For now because I'm not sure how to best handle inheritance, I'm duplicating a lot of my event handling within my subtypes.
10 Posted by Abel de Beer on 26 Oct, 2010 11:14 PM
Your example is quite accurate. If DogView and CatView extend AnimalView then your DogMediator and CatMediator can extend AnimalMediator. Here's what that would look like (semi-pseudo code hehe):
Note: The injectViewAs property supports Arrays from version 1.3.0 (I think).
Hope that answers your question.
11 Posted by rickcr on 27 Oct, 2010 12:08 AM
Thanks Abel!
You're pseudo code helped a lot.
The main thing I needed to do, not shown above, was to be sure that DogMediator's onRegister called super.onRegister(), since there were some common event listeners set up in the base class' onRegister that I needed.
Stray closed this discussion on 10 Feb, 2011 06:04 PM.