A user story:
The user of our app creates road trips. A roadtrip
is a sequential series of interesting destinations. Each destination
has some details about an activity or sight to see while there. In this way, our user defines two road trips where each trip had some unique destinations and some destinations common to both--e.g. both trips include The Smithsonian. The app maintains all updates in memory and only commits to the database when the user clicks save. The user actively updates both trips and can switch between them at will. At points in our app we’re dealing with the Smithsonian destination but we sometimes need to navigate up our object hierarchy from the destination to its containing road trip. The problem is that the destination takes part in two road trips.
RoadTrip1
|
+-Destination1
+-Destination2
+-Destination3
+-Smithsonian (A) //Navigate up to RoadTrip1
RoadTrip2
|
+-Destination4
+-Smithsonian (B) //Navigate up to RoadTrip2
+-Destination5
What's a good design pattern or data structure we could use to allow for backward navigation while assuring we have just one copy of our destination object?
Requirements:
My best idea so far is to wrap each destination object with a context object (similar to how linked lists wrap nodes). The context object would maintain a pointer to the parent from which it was originally fetched. We would deal with each destination always through its wrapper. I believe this would be either the Proxy or Decorator pattern (I lean toward Proxy). (Wouldn't this essentially be the same idea as how the jQuery object encompasses many elements and multiple jQuery objects share references to the same elements?)
I considered maintaining a "current road trip" context variable and use that for navigating from a destination up to its containing road trip. This isn't as reliable as the actual "fetching context". In fact, it's a completely different tack and I'm not sure I like it.
I remember having the same issue with ActiveRecord (though it's been a while since I worked with it). In AR, if I started with RoadTrip1 and then fetched its destinations I couldn't very well navigate from a destination back up to the road trip (via some sort of fetching context). Instead, I'd have both parents (road trips) to consider and no indicator as to how I got there. Right?
Have others run into this problem before--that is, wanting to navigate backwards where backward navigation is confused by many parents? Have you ever asked "from which parent did I arrive here?" How did you answer that?
I was able to work up the solution I was after using the proxy pattern. What I really wanted was the concept of a "fetching context". I wanted to know from which parent I originally fetched a model (a child having multiple parents).
The key to solving the problem was realizing maintaining a fetching context was the responsibility of our query objects not our models.
var activity = roadtrip.destinations().all().activities().first();
We start with a roadtrip
model and call the destinations
function. This function returns a query object. This query object is similar in design to Rails' Arel implementation in that it's lazy, not actually returning any records until you call all
, first
, each
, etc. The query object has a context
variable which points to its parent--the roadtrip
model.
The all
call returns a collection object whose context
points to the query from which it was invoked. Each item in the collection is a proxy (a queried item
) that wraps each underlying destination
model. The proxy maintains a reference to its collection. The easiest way to achieve this proxy is like so:
var proxied_destination = Object.create(destination);
In this way you can assign the context
to the proxy without affecting the original.
proxied_destination.context = collection;
This allows the "master" model to remain untouched and thus be identity mapped. This would not be possible if our model maintained direct reference to its collection since the model can take part in multiple result sets (we can run as many queries as we want) and, in my scenario, we expect only one context (parent).
We call activities
which provides us with another query object whose context is the proxied destination
. We call first
and rather than get a collection we get a proxied activity
having a context that points to the activities query object.
As such, using proxies allows us point up and thus "climb" the object hierarchy while still maintaining identity-mapped models that remain oblivious to that hierarchy and can easily take part in multiple collections (result sets).