Postal address, reusability, polymorphism and concerns in Ruby on Rails
Tags: rails, ruby
Published: 2021-02-09 17:03:33


## 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

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

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

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

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

    class Customer < ApplicationRecord
        has_one :collocation, as: :collocable
    end

After this we are able to:
![rails-console](https://dev-to-uploads.s3.amazonaws.com/i/xh4nwovh5sad7sara6hp.png)
 

## 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

But we would like to write things like:
 
    trainer.postal_address

instead of

    trainer.collocation.postal_address

The solution is:

    class Trainer < ApplicationRecord
        has_one :collocation, as: :collocable, dependent: :destroy
        has_one :postal_address, through: :collocation
    end

now we can write:

    trainer.postal_address

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

    has_one :postal_address, through: :collocation

### 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
And use (*include*) it in Customer model

    class Customer < ApplicationRecord
        include  Collocable
    end

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

    class Trainer < ApplicationRecord
        include  Collocable
    end

After this we can:

    customer.postal_address

and

    trainer.postal_address

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

> 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

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

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