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

Free Training By Email:
Building a Rails Engine

We will send udpates but we care about your data.
No spams.

Back to homepage