Judging design quality in ruby on rails
Judging Design Quality
Effective object-oriented design (OOD) in Ruby on Rails is crucial for maintaining scalable, efficient, and less costly applications. Contrary to historical metrics like SLOC (source lines of code), modern Ruby developers rely on more sophisticated methods to evaluate code quality, particularly focusing on OOD principles.
Misleading Metrics: SLOC
Historically, the quantity of code—measured by SLOC—was a misguided attempt to gauge productivity.
However, this metric often penalizes efficient programming and advantages verbose, inefficient coding practices. Consider the following example:
# app/models/user.rb
# Columns:
# id (integer, PK)
# name (string, indexed)
# email (string, unique indexed)
class User < ApplicationRecord
def display_name
name.blank? ? "No Name Provided" : name
end
end
While the display_name
method above is concise and efficient, a SLOC-focused metric might mistakenly undervalue the programmer’s effort compared to a less concise approach.
Current Metrics: OOD Principles
Modern Ruby tools assess code based on OOD principles, focusing on characteristics like modularity, encapsulation, and polymorphism.
These tools, however, should be used cautiously. High scores on OOD metrics don’t necessarily guarantee future ease of changes. For instance, over-engineering for potential, future requirements can lead to expensive adjustments when needs change. Below illustrates a bad versus a better OOD approach in a Rails model context:
Bad design: Over-anticipating future requirements
# app/models/project.rb
class Project < ApplicationRecord
has_many :tasks
# Over-designed status management:
# This type of nonsense happens when teams can't seem to understand that
# maybe the initial requirements could've been simplified.
def activate
update(status: 'active') if status == 'initialized'
end
def finalize
update(status: 'finished') if status == 'done'
end
end
Better design: Simplified and direct, just make sure to validate the statuses.
class Project < ApplicationRecord
has_many :tasks
# If this validator fails, the error will explain why, and future changes and errors are encapsulated here.
enum status: [ 'active', 'finished', 'failed' ]
end
class ProjectsController < ApplicationController
def update
@project.update(status: status)
end
end
In the above, the “bad” design preempts specific status changes, leading to rigid code susceptible to future changes.
The “better” design is simple and adaptable, maintaining ease of modification.
Utilizing OOD Metrics Wisely
OOD metrics are undeniably useful for identifying problematic code areas, but they’re not direct quality indicators.
They serve better as proxies, suggesting areas for deeper investigation. Below is an example of how metrics might indicate a problem:
# Running an OOD metrics report might reveal high coupling in this class:
# app/models/invoice.rb
# Columns: id (integer, PK), order_id (integer, FK, indexed), issued_on (date)
class Invoice < ApplicationRecord
belongs_to :order
# High coupling: Invoice class knows too much about Order and its internals
def total
order.line_items.sum(&:price) * (1 - order.discount)
end
end
The coupling shown above could direct developers to refactor towards better encapsulation and reduced interdependencies.
Recomended refactor
# app/models/invoice.rb
class Invoice < ApplicationRecord
belongs_to :order
has_many :items, through: :order
delegate :total, to: :order
end
# works
@invoice.total
# app/models/order.rb
class Order < ApplicationRecord
has_many :invoices
has_many :items, class_name: Orders::Items.to_s
delegate :total_price, to: :items, prefix: :items
def total
items_total_price * (1 - order.discount)
end
end
# works
@order.total
# app/models/items.rb
class Orders::Items < ApplicationRecord
belongs_to :order
scope :total_price, -> { sum(:price) }
end
# works and returns the sum using and SQL statement.
@orders_items.total_price
NOTE: Here the model
Orders::Items
could be subject to denormalization depending on system requirements.
Concluding Thoughts
Ultimately, the best metric might be “cost per feature” over a relevant time interval, balancing immediate needs against future costs—akin to managing technical debt.
Developers must use OOD metrics judiciously, employing them as a guide rather than a definitive measure, and always with a focus on the specific goals and context of their application.
Related posts
Building a Rails Engine