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
inUser
model does in ruby an operation that can be done in SQL.
Recommended refactoring
# 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.