Moof Mayeda
Jun 14, 2017 · 4 min read

If it looks like a duck and swims like a duck, but it doesn’t quack, is it a duck?

We have two models: Tracks and Clearances. Tracks are where we hold all of our songs for licensing. Clearances hold information about songs that we don’t own the rights to, but that we’ve helped a client secure a license for.

By many measures, Clearances look like Tracks — they have a title, an artist, and a composer. When a client licenses a Track or a Clearance, there are places in the app where we need to display song details, such as when automatically generating a license, invoice, or a receipt.

If they’re so alike, why use different models to represent them?

On the other hand, there are technical and business reasons to separate Tracks and Clearances. Tracks have a lot to them — they have an audio file, states, tags, and are designed to be searchable by our music supervisors and clients. Because we don’t own the rights to Clearances, they will never be searchable or live on our website. They are mostly only for display purposes.

In the past, when we needed to display the title of the song being licensed, we’d have to use different methods depending on whether or not it was a Clearance:

line_item.clearance ? line_item.clearance.track : line_item.track.display_name

Our app was littered with conditionals like this every time we needed to display the track title, artist, or composer.

Additionally, when we needed a track object created from clearance information (to pass into a serializer, for example), we created a temporary track from the clearance information:

# app/models/line_item.rbdef create_clearance_track 
artist = Artist.new name: self.clearance
bucket = TrackBucket.new artist: artist, name: self.clearance
track = Track.new title: self.clearance, composer: self.clearance, bucket: bucket
track
end

There’s hope for a better way

My goal was to clean up the conditionals and be able to simply call line_item.track.display_name whether the track was a Clearance or a regular Track.

This problem was a perfect set up to use the special case pattern, where a Clearance is a special case of a Track. Clearances needed a Track-like representation so that we could call Track methods and attributes on it.

# app/models/clearance_track.rbclass ClearanceTrack < Track
def initialize(args={})
@clearance = args.delete(:clearance)
super(args)
@title = clearance.track
@artist = Artist.new(name: clearance.artist)
@bucket = TrackBucket.new(artist: artist, name: clearance.track)
@composer = clearance.composer
end

attr_reader :clearance, :artist, :bucket, :title, :composer
def display_name
title
end
end

I created a new ClearanceTrack class that inherits from Track. Now instead of calling create_clearance_track, we could use ClearanceTrack.new(clearance: line_item.clearance). That would instantiate an instance of ClearanceTrack, taking the title, artist, and composer from the Clearance and properly assigning them to ClearanceTrack attributes.

This also meant we could write some model tests that would ensure the behavior we wanted:

# spec/models/clearance_track_spec.rb
describe ClearanceTrack do
let(:clearance) { create(:clearance) }
let(:track) { ClearanceTrack.new(clearance: clearance) }
it “has an artist whose name is the same as the clearance artist” do
expect(track.artist).to be_an(Artist)
expect(track.artist.name).to eq clearance.artist
end
it “belongs to a track bucket whose name is the same as the clearance track” do
expect(track.bucket).to be_a(TrackBucket)
expect(track.bucket.name).to eq clearance.track
end

it “has a :composer attribute that’s the same as the clearance composer” do
expect(track.composer).to eq clearance.composer
end
it “has a display name that’s the same as the clearance track” do
expect(track.display_name).to eq clearance.track
end
end

What can we do with this new ClearanceTrack class?

We still had to call ClearanceTrack.new when we wanted to instantiate a ClearanceTrack object. I wanted to be able to just call line_item.track and get a ClearanceTrack object. So I decided to override the track method being provided by a belongs_to association and instantiate the ClearanceTrack here. In a way, I took all those if line_item.clearance conditionals and collapsed them into this one instance.

# app/models/line_item.rbdef track
if self.clearance
ClearanceTrack.new(clearance: self.clearance)
else
super
end
end

This enabled me to freely call line_item.track and get back a Track or a ClearanceTrack. I completely removed the offending conditionals and simply used line_item.track.display_name, for example.

There’s a catch…

The only ‘gotcha’ was rendering a link to the track page. Since ClearanceTracks don’t have a track page (they are Plain Old Ruby Objects, so they don’t even have a track id), link_to would generate an error when attempting to render link_to line_item.track.display_name, [:manage, line_item.track].

So we needed a way to conditionally render the link. link_to_if to the rescue!

link_to_if line_item.track.has_link_in_portal?, line_item.track.display_name, [:manage, line_item.track]

Of course, for this to work we needed the has_link_in_portal? boolean on the Track and ClearanceTrack models.

# app/models/track.rb
def has_link_in_portal?
true
end
# app/models/clearance_track.rb
def has_link_in_portal?
false
end

Ta da! By implementing the special case pattern, I was able to get rid of dozens of conditionals all over our app. I could take this one more step to the database level. Clearances are still stored in their own table but they could be moved over to Track if I set up Single Table Inheritance. What do you think?

MarmoLabs

Shared discoveries, patterns, and how-to’s from the Marmoset software team to you.

Thanks to Emily Kingan and Ryan Rebo

Moof Mayeda

Written by

Organizer turned software engineer. I read+write about programming and social justice.

MarmoLabs

MarmoLabs

Shared discoveries, patterns, and how-to’s from the Marmoset software team to you.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade