In our last post, we talked about how we ensured the integrity of our data as we transitioned to a new version of our payment system. We touched upon the fact that we wanted to make the transition seamless for our end users, but didn’t go into detail about how we did that.
While we ported over much of the functionality of our old system to the new one, there were still many things we wanted to deprecate but couldn’t for various reasons (e.g. it was used by a 3rd party service, API, or we just didn’t have time to fully sunset it). We still needed to support these things in the interim, but really didn’t want to litter our shiny new code with legacy behavior that didn’t really fit into the new architecture. Enter shims.
A concept we found useful in helping us achieve this were shims: small libraries we could use to bridge the gap between the systems during the interim period.
For example, one of the models we obviated was the account model. This was a basic association on a user that looked like:
1 2 3 class User < ActiveRecord::Base has_one :account end
And our code was peppered with calls like:
1 2 3 if user.account.active_until_date > Date.today # do something end
In the new system, we might have ported the
active_until_date method to another model, deprecated it, or needed to reproduce it with more complex logic. So how could we handle this transparently so that the right methods would be called once a user was transitioned from the old system to the new system? We ended up creating a mixin that looked something like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 module AccountShim # Transparently compatible methods delegate :active_until_date, :to => :account_owner # Deprecated or more complex methods def do_backwards_compatible_thing if account_migrated? # something long and scary else account_owner.do_backwards_compatible_thing end end def do_old_thing account_migrated? ? nil : account_owner.do_old_thing end private def account_owner @account_owner ||= account_migrated? ? self.customer : self.account end def account_migrated? !!account_migrated end end
Once mixed in to our user class, it would delegate methods to the appropriate “account owner” – that is, the model that knew how to correctly respond to the method given the user’s current state. In our example, this was previously the Account class and subsequently the Customer class. The switch to the new association only happened once the user had been migrated (this was a flag that would be flipped after a migration had successfully been run on a user).
Our user class ended up looking something like:
1 2 3 class User < ActiveRecord::Base include AccountShim end
And the method calls were all replaced with:
Once the migration was 100% complete and the unneeded methods were fully deprecated or reproduced elsewhere, we went ahead and moved the delegations to the user and deleted this file – no external dependencies, no fuss, no muss.
Delegate your troubles away
Note that as we mentioned there were a lot of places where we had code that was written like:
In these cases, we shouldn’t have exposed the fact that the account model was how the user was getting this information. This is an implementation detail that should have been abstracted away – after all, the caller only cares about the fact that the user was active, not who the owner of that information is.
If we had originally done something like:
1 2 3 class User < ActiveRecord::Base delegate :active?, :to => :account end
Then we would have saved ourselves a lot of time picking through our codebase, finding these references, replacing them with the shim’d methods, and fixing tests. Lesson learned!