Object oriented design principles in rails
The Power of SOLID and DRY in Rails
In Ruby on Rails adhering to Object-Oriented Design (OOD) principles not only streamlines the development process but significantly improves maintainability and scalability.
These principles, particularly SOLID and DRY, guide developers in crafting robust, reusable code structures that prevent redundancy and promote modularity.
Illustrating Single Responsibility and DRY
Consider two versions of a Rails model, where one violates Single Responsibility Principle (SRP) and DRY, while the other adheres to them.
Poorly Designed Code
# app/models/invoice.rb
class Invoice < ApplicationRecord
belongs_to :customer
def print_invoice
# Generate PDF
end
def calculate_total
# Total calculation logic
end
def send_to_customer
# Email sending logic
end
end
Here, the Invoice
class is overburdened with multiple responsibilities:
- managing invoice properties
- business logic for total calculations
- formatting output
- communication mechanisms
This design makes the class difficult to maintain and extends the reasons for its modification, which violates both SRP and DRY.
Improved OOD-Compliant Code
# app/models/invoice.rb
class Invoice < ApplicationRecord
delegate :print, to: :processor
delegate :total, to: :calculator
belongs_to :customer
def processor
InvoicePrinter.new(self)
end
def calculator
InvoiceCalculator.new(self)
end
end
# app/services/invoice_printer.rb
class InvoiceProcessor
def initialize(invoice)
@invoice = invoice
end
def print
# Generate PDF logic, could be async...
end
end
# app/services/invoice_calculator.rb
class InvoiceCalculator
def initialize(invoice)
@invoice = invoice
end
def total
# Total calculation logic
end
end
# app/mailers/invoice_mailer.rb
class InvoiceMailer < ApplicationMailer
def send_invoice(invoice)
@invoice = invoice
mail(to: @invoice.customer.email, subject: 'Your Invoice')
end
end
NOTE: I agree with you that in this example, it’s quite an overkill solution. But the goal is to showcase a method to separate responsibilities using the delegate method. In real life, the print method could just be a worker. As always, many answers will come from defining the requirements for the system more clearly.
By segregating the functionalities into focused classes (InvoicePrinter
for printing and InvoiceMailer
for emailing), each class now adheres to SRP and enhances code reuse and testing capabilities.
This is why Rails, on average, is more OOD compliant than Node or other non-opinionated frameworks. No one in the Rails community would use email logic in a model; they would just use the Mailer class.
Can you see how being a Rails developer leads to understanding OOD sooner?
Open/Closed Principle in Rails Controllers
Adhering to the Open/Closed Principle (OCP) in Rails controllers fosters implementation flexibility and minimizes the impact of change.
Example of Violating OCP
# app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
def create
@invoice = Invoice.new(invoice_params)
if @invoice.save
# Directly embedding PDF generation and sending email
InvoicePrinter.new(@invoice).print
InvoiceMailer.send_invoice(@invoice).deliver_later
redirect_to @invoice, notice: 'Invoice was successfully created.'
else
render :new
end
end
end
This approach directly embeds the PDF generation and emailing within the controller, making it less resilient to changes in the printing or emailing process.
OCP-Compliant Version
# app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
def create
@invoice = Invoice.new(invoice_params)
if @invoice.save
ProcessInvoiceJob.perform_later(@invoice.id)
redirect_to @invoice, notice: 'Invoice was successfully created.'
else
render :new
end
end
end
# app/jobs/process_invoice_job.rb
class ProcessInvoiceJob < ApplicationJob
def perform(invoice_id)
invoice = Invoice.find(invoice_id)
InvoicePrinter.new(invoice).print
InvoiceMailer.send_invoice(invoice).deliver_later
end
end
In the refined version, the controller’s responsibility is scaled down to merely handling HTTP transactions, while the business logic of processing invoices is delegated to a background job.
This adherence to OCP allows future modifications to invoice processing to occur without impacting the controller, enhancing the system’s resilience to change.
Conclusion
Think about your code like a manufacturing process and group responsibility into functions/classes.
Software is the only industry where you are asked to create a washing machine on day one, then the next day it’s supposed to fly and cook pancakes.
The more you ground your code in reality, the more you will be able to identify requirements that are delusional and ultimately just undefined problems.
Related posts
Building a Rails Engine