Photo

Hi, I'm Aaron.

Introducing common API with Aliases

Scenario

Let’s say that you have an application that has three very similar models, CompactDisc, CassetteTape, and VinylRecord. They all have very similar behaviors, and are all related to one another, though they each have separate tables. STI may or may not be possible.

Code smell Similar to “data clump”, “feature envy”; though in this case it’s models that are similar enough to always be aggregated together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# app/models/compact_disc.rb
class CompactDisc < ApplicationRecord
  has_many :tracks
end

# app/models/cassette_tape.rb
class CassetteTape < ApplicationRecord
  has_many :songs, through: :sides
end

# app/models/vinyl_record.rb
class VinylRecord < ApplicationRecord
  has_many :grooves, through: :record_sides
end

They might be collected together like this (where it’s happening isn’t really important):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@compact_discs = CompactDisc.all
@cassette_tapes = CassetteTape.all
@vinyl_records = VinylRecord.all

(@compact_discs + @cassette_tapes + @vinyl_records).each do |album|
  case album.class
    when CompactDisc
      render partial: "tracklist", collection: album.tracks
    when CassetteTape
      render partial: "liner_notes", collection: album.songs
    when VinylRecord
      render partial: "tracklist", collection: album.grooves
  end
end

Whether or not this could actually be refactored into an STI relationship is unknown, but we will assume it cannot be, for one reason or another. A case statement, particularly with type-checking, is generally a code smell on its own.

Let’s assume that what we would like the block above to look like is this:

1
2
3
4
5
6
7
@compact_discs = CompactDisc.all
@cassette_tapes = CassetteTape.all
@vinyl_records = VinylRecord.all

(@compact_discs + @cassette_tapes + @vinyl_records).each do |album|
  render partial: "album", object: album
end

and that the _album.html.erb partial looks like this (just to show that we’re not just hiding the type checking). This is simplified for clarity.

1
2
3
4
# app/views/albums/_album.html.erb
album.songs.each do |song|
  render partial: "song", object: song
end

The most direct change would be to modify the association name, but because this is a legacy app, we want to make the least-impactful change. That We can use the :alias method here. So we might change the models from earlier to look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app/models/compact_disc.rb
class CompactDisc < ApplicationRecord
  has_many :tracks
  alias :songs, :tracks
end

# app/models/cassette_tape.rb
class CassetteTape < ApplicationRecord
  has_many :songs, through: :sides # unchanged
end

# app/models/vinyl_record.rb
class VinylRecord < ApplicationRecord
  has_many :grooves, through: :record_sides
  alias :songs, :grooves
end

We can assume that Track and Song quack in similar enough ways that they can function interchangeably in the _song partial. (If not, additional alias can be used)