Encapsulate For Easy Refactors
By: Jay Shah
Tags: Rails, Encapsulation, Programming, Ruby, Refactor

An application is a living, breathing code base that will continually change over time. As the application evolves, early decisions won’t scale, and shortcuts taken will reveal technical debt. When the time comes to address these problems, one thing you can start doing today to make refactoring tomorrow easier is using encapsulation.

Example

Imagine your application has a User model, and the User can have a role. The role is stored as a column on the model.

# == Schema Information

#

# Table name: users

#

# id :bigint(8) not null, primary key

# email :string default(""), not null

# first_name :string

# last_name :string

# role :string

# created_at :datetime not null

# updated_at :datetime not null

#

class User < ApplicationRecord

end

You might have actions in your controller where you check if a user has a specific role. You might decide to directly access the role attribute and compare it with the role you care about:

class ItemController < ApplicationController

def update

if user.role == 'admin' || user.role == 'operations'

update_item(params[:item_id])

end

end

end

While this seems harmless at first, it becomes painful to refactor when you have to extend the relationship so a user can have many roles.

Extending Role To Its Own Model

Imagine your product manager asks you to support users having multiple roles. You will have to move the role attribute from the User model to its own model.

Models

The resulting model design might look like the following. The User model now has a has_many relationship through a joining class ( UserRole ) to a Role model.

User

# == Schema Information

#

# Table name: users

#

# id :bigint(8) not null, primary key

# email :string default(""), not null

# first_name :string

# last_name :string

# role :string

# created_at :datetime not null

# updated_at :datetime not null

#

class User < ApplicationRecord

has_many :user_roles, dependent: :destroy

has_many :roles, through: :user_roles

end

UserRole (Joining Class)

# == Schema Information

#

# Table name: user_roles

#

# id :bigint(8) not null, primary key

# created_at :datetime not null

# updated_at :datetime not null

# role_id :bigint(8)

# user_id :bigint(8)

#

class UserRole < ApplicationRecord

belongs_to :user

belongs_to :role

end

Role

# == Schema Information

#

# Table name: users

#

# id :bigint(8) not null, primary key

# name :string

# created_at :datetime not null

# updated_at :datetime not null

#

class Role < ApplicationRecord

has_many :user_roles, dependent: :destroy

has_many :users, through: :user_roles

end

Usages

Instead of checking if a user.role is equal to a role, we'll now check if a specific role is in the list of roles attached to a user.

admin_role = Role.create(name: 'admin')

operations_role = Role.create(name : 'operations')

user = User.find(1)



# add roles to user

user.roles << admin_role

user.roles << operations_role

user.roles # [<Role name: 'admin'>, <Role name: 'operations'>]



# check if a user is an admin

user.roles.exists?(name: 'admin') # true

Refactoring Usages

When we refactor the ItemController to use these new models and methods, we first want to bring all methods into the User class and then update usages.

# Encapsulate all User role related methods.

# New feature is also gated behind a feature flag.



class User < ApplicationRecord

has_many :user_roles, dependent: :destroy

has_many :roles, through: :user_roles



def has_role?(role_name)

if feature_flag_on?

roles.exists?(name: role_name)

else

role == role_name

end

end



def admin?

has_role?('admin')

end



def operations?

has_role?('operations')

end

end