Applying SOLID Principles in Ruby on Rails

Bhavesh Saluja
4 min readJul 8, 2024

--

Imagine your Rails application — a magnificent structure teeming with functionality. But will it crumble under pressure or stand the test of time? Enter the SOLID principles, a set of five fundamental design principles that empower you to craft robust, maintainable, and scalable Rails applications. This blog equips you with the knowledge to apply these principles, ensuring your applications are built on solid foundations.

SOLID principles in Rails

Need for SOLID:

  • Improved Maintainability: SOLID principles promote code that is easier to understand, modify, and extend, reducing long-term maintenance costs.
  • Enhanced Reusability: By fostering code modularity, SOLID principles enable the creation of reusable components, promoting code efficiency.
  • Reduced Complexity: These principles guide you towards simpler, more concise code, leading to fewer bugs and easier debugging.

Exploring the SOLID Principles:

Single Responsibility Principle (SRP):

A class should have one, and only one, reason to change. This promotes focused, well-defined classes that are easier to understand and test.

Example (SRP Violation):

class User
def initialize(name, email)
@name = name
@email = email
end

def send_welcome_email
# Email sending logic
end

def update_password(new_password)
# Password update logic
end
end

Improved Version (Following SRP):

class User
def initialize(name, email)
@name = name
@email = email
end
end


class UserMailer
def send_welcome_email(user)
# Email sending logic using user information
end
end

Open/Closed Principle (OCP):

Software entities (classes, modules) should be open for extension, but closed for modification. This allows you to add new functionality without altering existing code.

Example (OCP Violation):

class OrderProcessor
def process(order)
# Existing order processing logic

if order.type == "VIP"
# Additional logic for VIP orders (tightly coupled)
end
end
end

Improved Version (Following OCP):

class OrderProcessor
def process(order)
# Existing order processing logic
end
end

class VipOrderProcessor < OrderProcessor
def process(order)
super # Call base class processing
# Additional logic specific to VIP orders
end
end

Liskov Substitution Principle (LSP):

Subtypes (derived classes) should be substitutable for their base types (parent classes) without altering the program’s correctness. This ensures consistent behavior across class hierarchies.

Example (LSP Violation):

# Before applying LSP
class Rectangle
attr_accessor :width, :height

def initialize(width, height)
@width = width
@height = height
end

def area
@width * @height
end
end

class Square < Rectangle
def initialize(side_length)
super(side_length, side_length)
end
end

Improved Version (Following LSP):

# After applying LSP
class Shape
def area
raise NotImplementedError, 'Subclasses must implement the area method'
end
end

class Rectangle < Shape
attr_accessor :width, :height

def initialize(width, height)
@width = width
@height = height
end

def area
@width * @height
end
end

class Square < Shape
attr_accessor :side_length

def initialize(side_length)
@side_length = side_length
end

def area
@side_length * @side_length
end
end

Interface Segregation Principle (ISP):

Clients (objects) should not be forced to depend on methods they don’t use. Break down large interfaces into smaller, more specific ones.

Example (ISP Violation):

# Before applying ISP
class Printer
def print_document(document)
# print logic
end

def fax_document(document)
# fax logic
end
end

Improved Version (Following ISP):

# After applying ISP
module Printer
def print_document(document)
# print logic
end
end

module Fax
def fax_document(document)
# fax logic
end
end

class BasicPrinter
include Printer
end

class AdvancedPrinter
include Printer
include Fax
end

Dependency Inversion Principle (DIP):

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. This promotes loosely coupled modules that are easier to test and maintain.

Example (DIP Violation):

# Before applying DIP
class PaymentService
def process_payment(amount)
# process payment using specific payment gateway
end
end

Improved Version (Following DIP):

# After applying DIP
class PaymentService
def initialize(payment_processor)
@payment_processor = payment_processor
end

def process_payment(amount)
@payment_processor.process(amount)
end
end

class StripeProcessor
def process(amount)
# process payment using Stripe
end
end

class PayPalProcessor
def process(amount)
# process payment using PayPal
end
end

# Usage
stripe_service = PaymentService.new(StripeProcessor.new)
stripe_service.process_payment(100)

paypal_service = PaymentService.new(PayPalProcessor.new)
paypal_service.process_payment(200)

Exploring Advanced SOLID Techniques:

  • Design Patterns: Leverage design patterns like Factory Method and Strategy to further enhance code reusability and flexibility.
  • Dependency Injection: Utilize dependency injection frameworks to manage dependencies between objects, promoting better testability.
  • Testing: Write comprehensive unit and integration tests to ensure your SOLID-based code functions as expected.

Conclusion:

By embracing the SOLID principles in your Rails development journey, you unlock a powerful set of tools for building applications that are not only functional but also maintainable, scalable, and resilient. Remember, these principles guide you towards well-defined classes, code reusability, and reduced complexity. While exploring advanced techniques, prioritize writing clean, testable code. With SOLID principles as your guiding light, you can craft applications that stand the test of time, evolving gracefully as your project grows.

--

--