Ruby on Rails and the Art of Object-Oriented Design


Emphasizing Maintainability and Scalability Through Design

Ruby on Rails, by virtue of its opinionated nature, guides developers towards certain design patterns and practices.

However, even within these constraints, there’s significant room for design decisions that impact the future maintainability and scalability of applications.

Let’s explore how to leverage object-oriented design principles to craft better Rails applications.

PS: Most perf issues comes from bad designs (i.e: bad choices)…

Understanding the Cost of Poor Design

Consider the long-term implications of the initial code arrangement.

Rails applications that start with a tight, interdependent object mesh often face rising costs in maintenance as they scale.

This problem intensifies when the codebase expands to accommodate new features.

The principle here is straightforward:

The cost of modifying an application increases disproportionately to its complexity.

Example of poorly designed Rails code

# app/models/user.rb
class User < ApplicationRecord
  has_many :projects

  def total_project_hours
    projects.sum { |p| p.total_hours }
  end
end
# app/models/project.rb
class Project < ApplicationRecord
  belongs_to :user
  has_many :tasks

  def total_hours
    tasks.sum(:hours)
  end
end
class Task < ApplicationRecord
  belongs_to :project
end

In the above User model, notice the method total_project_hours, which traverses through projects to calculate hours.

This not only violates the Single Responsibility Principle by placing additional responsibility on the User model but also creates a potential performance bottleneck as the number of projects grows:

  • total_project_hours in User model does in ruby an operation that can be done in SQL.
# app/models/user.rb
class User < ApplicationRecord
  has_many :projects
  has_many :tasks, through: :projects

  delegate :sum_hours, to: :tasks, prefix: :tasks
end
# app/models/project.rb
class Project < ApplicationRecord
  belongs_to :user
  has_many :tasks
end
class Task < ApplicationRecord
  belongs_to :project

  scope :sum_hours, -> { sum(:hours) }
end

Now total_project_hours does not exist anymore and you can access the same data using:

  • @user.tasks_sum_hours

This will give you the sum of all the tasks in all the projects for one user.

Moving total_hours to the Project model aligns with the Single Responsibility Principle, simplifying the User model and delegating the responsibility for calculating hours to the Project model.

Conclusion

If you want to learn how to refactor like a pro, read this:

The essence of good object-oriented design in Rails is not merely about adhering to its conventions but understanding and applying design principles that safeguard future scalability and maintainability.

By structuring code with an eye on future change, you reduce the costs and complexities involved in maintaining and evolving your Rails application.


Related posts

Back to homepage