DEV Community

loading...
Cover image for Postal address, reusability, polymorphism and concerns in Ruby on Rails

Postal address, reusability, polymorphism and concerns in Ruby on Rails

montells profile image Michel Sánchez Montells ・3 min read

Postal address as a requirement.

Like many of you, I have several models in my current project, such as Customer, Trainer, Building, etc...

A client came recently with a new and very common requirement:

"I want to store and show the postal address of the customers."
Take into account.

  • One Customer has only one postal address.
  • One postal address can be shared by many customers.

I know. You know. He will come shortly with the second, new and very common requirement:

"I want to store and show the postal address of the trainers."

I googled how to solve this problem and after do not find out about this I decided to write it down for sharing with the readers my solution for this very common but not new problem.

Polymorphism

My solution needs to cover the second requirement from the beginning. Let's start:

class CreatePostalAddresses < ActiveRecord::Migration[6.0]
    def change
        create_table :postal_addresses do |t|
            t.string :street
            t.string :city
            t.string :country
            t.string :zipcode
            t.string :number
            t.string :country_ISO2

            t.timestamps
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

The postal_addresses table is unavoidable and if we want to make this reusable and robust you can see no explicit relations are involved in this table.

Keeping in mind we want this postal address in a very clean way available for many other models. Then WE ARE NOT GOING TO do this:

class PostalAddress < ApplicationRecord
    has_many :customers
end

class Customer < ApplicationRecord
    belongs_to :postal_address
end
Enter fullscreen mode Exit fullscreen mode

This requires to include the postal_address_id in every customer. This solution will drive us to include another postal_address_id column in every table we would like in the future to support postal_address.

So are going to create the join model/table between postal_address and whatever model, using the polymorphic relations provided by ActiveRecord:

class Collocation < ApplicationRecord
    belongs_to :collocable, polymorphic: true
    belongs_to :postal_address
end
Enter fullscreen mode Exit fullscreen mode

The corresponding migration looks like:

class CreateCollocations < ActiveRecord::Migration[6.0]
    def change
        create_table :collocations do |t|
          t.references :collocable, polymorphic: true, index: true
          t.references :postal_address, foreign_key: true
          t.timestamps
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

Finally just setup the relations in the model asked in the first requirement:

class Customer < ApplicationRecord
    has_one :collocation, as: :collocable
end
Enter fullscreen mode Exit fullscreen mode

After this we are able to:
rails-console

Reusability via Concerns

With this approach, we already can use PostalAddress in other models like Trainer doing only this:

class Trainer < ApplicationRecord
    has_one :collocation, as: :collocable
end
Enter fullscreen mode Exit fullscreen mode

But we would like to write things like:

trainer.postal_address
Enter fullscreen mode Exit fullscreen mode

instead of

trainer.collocation.postal_address
Enter fullscreen mode Exit fullscreen mode

The solution is:

class Trainer < ApplicationRecord
    has_one :collocation, as: :collocable, dependent: :destroy
    has_one :postal_address, through: :collocation
end
Enter fullscreen mode Exit fullscreen mode

now we can write:

trainer.postal_address
Enter fullscreen mode Exit fullscreen mode

BUT we have another line that should be repeated in every Collocable model.

has_one :postal_address, through: :collocation
Enter fullscreen mode Exit fullscreen mode

Concerns to the rescue

Let's create the file for the Collocable concern

app/models/concerns/collocable.rb

module Collocable
    extend ActiveSupport::Concern

    included do
        has_one :collocation, as: :collocable, dependent: :destroy
        has_one :postal_address, through: :collocation
    end
end
Enter fullscreen mode Exit fullscreen mode

And use (include) it in Customer model

class Customer < ApplicationRecord
    include  Collocable
end
Enter fullscreen mode Exit fullscreen mode

In case we need in the future to have a postal address for Trainer we can:

class Trainer < ApplicationRecord
    include  Collocable
end
Enter fullscreen mode Exit fullscreen mode

After this we can:

customer.postal_address
Enter fullscreen mode Exit fullscreen mode

and

trainer.postal_address
Enter fullscreen mode Exit fullscreen mode

Now we are using polymorphism and concerns and we are in a very comfortable situation for adding more functionalities to our postal_address features without touching our collocable models.

Imagine we want to have postal_addres.full_addres method and be able to write trainer.full_address.

Adding method to our PostalAddress model

class PostalAddress < ApplicationRecord
    def full_address
        "#{number} #{street}, #{zipcode} #{city}, #{country_iso2}"
    end
end
Enter fullscreen mode Exit fullscreen mode

Adding features to our Collocable concern

module Collocable
    extend ActiveSupport::Concern

    included do
        has_one :collocation, as: :collocable, dependent: :destroy
        has_one :postal_address, through: :collocation

        delegate :full_addres, to: :postal_address, allow_nil: true
    end
end
Enter fullscreen mode Exit fullscreen mode

We can improve now our collocable feature as much as we want. When you add functionalities to collocable you should avoid including dependencies from the models who pretend to be collocable.

And in case of the next week my client comes with:

"I want to store and show the postal address of the Building."

I know and you know:

class Building < ApplicationRecord
    include  Collocable
end
Enter fullscreen mode Exit fullscreen mode

git commit -am 'adding the postal address to buildings'; git push origin master

Discussion (0)

pic
Editor guide