Changing a Getter behavior for better polymorphism
Polymorphic associations are awesome. In rails, even better.
It's so easy to use, there is no excuse to do without them. But sometimes, it's so easy and all pre-made for you, that it's tricky to decide how to play/modify some behavior.
Let's say you have an airline, and you want to save the reason why people are canceling a flight. Pretty straight forward, right?
First you create your new model, let's call it CancelReason.
class CancelReason < ActiveRecord::Base
belongs_to :flight
attr_accessor :reason, :flight_id
end
class Flight < ActiveRecord::Base
has_many :cancel_reasons
end
class FlightsController < ApplicationController
def cancel
…
self.cancel_reasons.create :reason => params[:reason]
…
end
end
So now, when one of your customer cancels a flight, you ask why and it saves a CancelReason linked to you flight. Then if you want to pull stats on what is the cause for most cancellations, you can just retreive all your reasons and see.
Cancel all the things!
But let's introduce something else; Trips. A Trip is a group of flights, either because your customer had a stop to go to his destination, or because he wants to go round the world, etc.
Trips can be cancelled.
class Trip < ActiveRecord::Base
has_many :flights
end
class TripsController < ApplicationController
def cancel
@trip.jobs.each do |job|
job.cancel
end
end
end
Again here, nothing's wrong. Somebody cancels a trip, it cancels all the flights; You still could ask for a reason, and the reason would get added to all the flights cancelled.
But this introduces a problem;
Let's say I book a flight to go to Perth, Australia. Maybe my trip will have 3 flights; One from NYC to SF. Then SF to Syndey. Then Sydney to Perth.
I loose my job, and cancel my flight, giving as a reason "too expensive".
Then somebody from Melbourne books a flight to Perth, which is direct, but have troubles with customer service and decides to cancel, giving as an excuse "bad experience".
When the airline data analysts will make statistics on why people are canceling, they are going to get all the reasons and group them and count them;
3 found the service too expensive, 1 had a bad experience. Clearly, we need to lower our prices, and for now on, the customer service might not be urgent.
HOW INNOCENT! Just because my trip had 3 flights, their data is all messed up!
A hint of polymorphism, please!
So, instead of adding a reason to flights when a trip is cancelled, let's make CancelReason polymorphic and add a cancel reason to a trip itself!
class CancelReason < ActiveRecord::Base
belongs_to :thing, polymorphic: true
attr_accesor :reason, :thing_id, :thing_type
end
class Flight < ActiveRecord::Base
has_many :cancel_reasons, :as => :thing
end
class Trip < ActiveRecord::Base
has_many :cancel_reasons, :as => :thing
end
class TripsController < ApplicationController
def cancel
@trip.cancel_reasons.create reason: params[:reasons]
end
end
Good! So now, when we cancel a trip, it will create a reason for the trip itself, and not for the flights anymore (I'll let you work around the flights controller to not create any reason when they are being cancelled as part of a trip's cancellation).
So now, when our data analysts will call all the reasons, only one reason for "too expensive" will appear, as it is supposed to.
But now, if you get one of the flights from the cancelled trip, and call @flight.cancel_reasons on it, you'll have nothing!
alias_method ftw
So you want to modify you cancelreasons getter method to return you the flight's cancelreason, unless the flight is part of a trip, and in that case, it should return the trip's cancel reasons!
class Flight < ActiveRecord::Base
alias_method :original_cancel_reasons, :cancel_reasons
def cancel_reasons
if self.original_cancel_reasons.present?
original_cancel_reasons
else
self.trip.cancel_reasons
end
end
end
Here, we check if the flight has its own cancelreasons. If not, it might be because it's part of a trip, so we will return the trip's cancelreasons!
And tada, you now have a fully functioning polymorphism with some method aliasing to keep the code as clean as possible!