Duck typing is a style of programming that relies on what an object does, rather than on what it is. Avoiding class dependencies results in highly flexible code. Ruby’s conversion protocols, used throughout the core and standard libraries, are a great example of the power of duck typing. In this post, we’ll look at two of Ruby’s conversion protocols: loose and strict.
In a typical Rails app, one ActiveRecord model tends to accumulate a lot of associations and related methods. This is usually the
User class; e.g., the
User has many posts, comments, contacts, projects, etc. It’s also common to have a few instance methods to filter these associations, e.g.,
Soon this God class becomes overwhelmed by all of these responsibilities. The intent of this refactoring is to move this behavior to the associated class.
Knowing Too Much
The following ActiveRecord model has a few associations (to keep this example simple), and an instance method to filter one of them.
class User < ActiveRecord::Base belongs_to :account has_many :projects has_many :contacts def active_projects projects.where(active: true) end end
User#active_projects seems to know too much about the domain's concept of an active project. Let's move it to the
User#active_projects can be converted to a class method on
class Project < ActiveRecord::Base def self.active_projects_for(user_id) where(user_id: user_id) .where(active: true) end end
We now need to update senders, e.g.,
ProjectsController#index, to use this new class method.
class ProjectsController < ActionController::Base def index @projects = current_user.active_projects end end
class ProjectsController < ActionController::Base def index @projects = Project.active_projects_for(current_user.id) end end
Responsibilities in Their Right Place
We can now remove the
projects association and related instance method from
User becomes simpler, and responsibilities feel like they're in their right place.
Replace Class Query Method with Query Object
If the associated class starts to accumulate too much behavior, or we just don't like class methods, then we could introduce a query object.
class ActiveProjectsQuery def initialize(relation = Project.scoped) @relation = relation end def for(user_id) @relation .where(user_id: user_id) .where(active: true) end end
Question Bidirectional Associations
When a new feature requires an ActiveRecord association, avoid the tendency to automatically make it bidirectional. Bidirectional associations often result in too many parent class responsibilities. Instead, implement these responsibilities in the associated class. It's more work, but your classes will stay small, cohesive, and ungodlike.