CrackedRuby CrackedRuby

Overview

SOLID represents five design principles for object-oriented software development introduced by Robert C. Martin in the early 2000s. These principles guide developers toward creating systems where classes have clear responsibilities, remain open to extension without modification, maintain substitutable hierarchies, define focused interfaces, and depend on abstractions rather than concrete implementations.

The acronym SOLID combines five distinct principles: Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle. Each principle addresses specific aspects of class design and interaction patterns that lead to code brittleness, tight coupling, and maintenance difficulties when violated.

Software systems that follow SOLID principles exhibit several characteristics. Classes remain small and focused on specific concerns. Changes to requirements affect minimal portions of the codebase. New features integrate through extension rather than modification of existing code. Objects can be replaced with their subtypes without breaking functionality. Dependencies flow toward stable abstractions rather than volatile concrete implementations.

# Violation of multiple SOLID principles
class UserManager
  def initialize
    @db = MySQL.new('localhost', 'users')
  end
  
  def create_user(name, email, role)
    user = { name: name, email: email, role: role }
    @db.execute("INSERT INTO users VALUES ...")
    send_welcome_email(email)
    log_user_creation(name)
    update_analytics(role)
  end
  
  def delete_user(id)
    @db.execute("DELETE FROM users WHERE id = #{id}")
    send_goodbye_email(@db.query("SELECT email ..."))
  end
  
  private
  
  def send_welcome_email(email)
    # Email sending logic
  end
  
  def log_user_creation(name)
    # Logging logic
  end
  
  def update_analytics(role)
    # Analytics logic
  end
end

This example violates multiple SOLID principles. The class handles user persistence, email notifications, logging, and analytics. It depends on the concrete MySQL implementation. Adding support for PostgreSQL or email service changes requires modifying this class. Testing becomes difficult because all dependencies are hardcoded.

Key Principles

Single Responsibility Principle states that a class should have one reason to change. A class exhibits single responsibility when changes to business requirements affect that class only if those requirements relate to its primary concern. The principle does not mean a class should do only one thing, but rather that all things it does should relate to a single conceptual responsibility.

Classes violate SRP when they combine multiple concerns such as business logic with persistence, or data processing with presentation. These combinations create tight coupling between unrelated aspects of the system. Changes to database schemas affect business logic classes. Changes to report formatting affect calculation classes. Each additional responsibility increases the likelihood that changes will ripple through the codebase.

Identifying responsibilities requires examining why a class might need to change. A User class that handles validation, persistence, and email notifications has three reasons to change: validation rule changes, database schema changes, or email template changes. Splitting these into User, UserRepository, and UserNotifier classes isolates each responsibility.

Open/Closed Principle states that software entities should be open for extension but closed for modification. Classes achieve this by defining stable abstractions that remain unchanged while allowing new behaviors through inheritance, composition, or polymorphism. The principle prevents cascading changes through a codebase when adding new features.

The principle does not prohibit all modifications. Bug fixes and refactoring constitute legitimate reasons to modify existing classes. The restriction applies specifically to adding new features or behaviors. When adding a new payment method, extending a payment processing system should not require modifying existing payment handler code.

Achieving open/closed compliance requires anticipating variation points in the system. Not every aspect of a class needs extensibility. Classes should be closed to modifications that add new feature variations but open to changes that fix defects or improve implementation quality. Strategic use of abstract classes, modules, and dependency injection enables this balance.

Liskov Substitution Principle requires that objects of a subtype be substitutable for objects of their supertype without breaking program correctness. Subtypes must honor the contracts established by their supertypes, including preconditions, postconditions, and invariants. Violations occur when subtypes strengthen preconditions, weaken postconditions, or alter expected behaviors.

The principle extends beyond simple type compatibility. A Square class that inherits from Rectangle might seem valid since squares are mathematically rectangles. However, if Rectangle allows independent width and height modifications, Square must violate this contract to maintain its square property. Code expecting Rectangle behavior breaks when receiving a Square instance.

Conformance requires careful contract design. Supertypes establish behavioral contracts through method signatures, return types, exception specifications, and documented constraints. Subtypes must maintain these contracts while potentially adding new capabilities. A method that accepts a supertype parameter should function correctly with any subtype instance without special handling.

Interface Segregation Principle states that clients should not depend on interfaces they do not use. Large interfaces force implementing classes to provide methods irrelevant to their purpose. Changes to interface methods affect all implementers even when those changes relate to functionality they do not need.

The principle advocates for cohesive, focused interfaces over monolithic ones. A Worker interface combining work(), eat(), and sleep() methods forces robot workers to implement eating and sleeping. Splitting into Workable, Eatable, and Sleepable interfaces allows classes to implement only relevant behaviors.

Ruby's duck typing and module system provide natural ISP compliance. Classes implement only the methods they need without declaring interface conformance. Modules allow mixing specific capabilities without inheriting unrelated methods. The principle remains relevant for designing public APIs and module interfaces.

Dependency Inversion Principle requires that high-level modules not depend on low-level modules, but both depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions. This inverts traditional procedural design where high-level policy depends on low-level implementation.

Traditional layered architectures create dependencies flowing downward from business logic to infrastructure. Changes to low-level components force changes in high-level policy. DIP reverses this flow by having business logic define interfaces that infrastructure implements. Business logic depends on abstractions it controls, not concrete infrastructure implementations.

The principle does not eliminate all concrete dependencies. Applications must instantiate concrete classes somewhere. Dependency injection, factory patterns, and composition roots concentrate these decisions in specific locations while the bulk of the codebase depends on abstractions.

Ruby Implementation

Ruby's dynamic nature and module system provide unique approaches to implementing SOLID principles. Duck typing reduces the need for explicit interface declarations while still enabling polymorphic behavior. Modules allow fine-grained composition of responsibilities. First-class functions and blocks facilitate dependency injection and strategy patterns.

Single Responsibility in Ruby:

# Focused user class handles only user data and business rules
class User
  attr_reader :id, :name, :email
  
  def initialize(id:, name:, email:)
    @id = id
    @name = name
    @email = email
  end
  
  def valid?
    email_valid? && name_present?
  end
  
  private
  
  def email_valid?
    email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
  end
  
  def name_present?
    name && !name.strip.empty?
  end
end

# Repository handles persistence concerns
class UserRepository
  def initialize(database)
    @database = database
  end
  
  def save(user)
    @database.execute(
      'INSERT INTO users (id, name, email) VALUES (?, ?, ?)',
      user.id, user.name, user.email
    )
  end
  
  def find(id)
    row = @database.query('SELECT * FROM users WHERE id = ?', id).first
    User.new(id: row['id'], name: row['name'], email: row['email']) if row
  end
  
  def delete(id)
    @database.execute('DELETE FROM users WHERE id = ?', id)
  end
end

# Notifier handles communication
class UserNotifier
  def initialize(mailer)
    @mailer = mailer
  end
  
  def welcome(user)
    @mailer.send(
      to: user.email,
      subject: 'Welcome',
      body: "Welcome #{user.name}!"
    )
  end
  
  def goodbye(user)
    @mailer.send(
      to: user.email,
      subject: 'Goodbye',
      body: "Sorry to see you go, #{user.name}."
    )
  end
end

Open/Closed with Ruby modules and polymorphism:

# Abstract payment processor defines contract
class PaymentProcessor
  def process(amount)
    raise NotImplementedError
  end
end

# Concrete implementations extend without modifying base
class CreditCardProcessor < PaymentProcessor
  def initialize(card_number, cvv)
    @card_number = card_number
    @cvv = cvv
  end
  
  def process(amount)
    # Credit card processing logic
    { status: :success, transaction_id: generate_id, amount: amount }
  end
  
  private
  
  def generate_id
    SecureRandom.hex(16)
  end
end

class PayPalProcessor < PaymentProcessor
  def initialize(email, token)
    @email = email
    @token = token
  end
  
  def process(amount)
    # PayPal API call
    { status: :success, transaction_id: paypal_transaction_id, amount: amount }
  end
  
  private
  
  def paypal_transaction_id
    # PayPal-specific ID generation
    "PP-#{SecureRandom.hex(12)}"
  end
end

# Payment service closed to modification, open to extension
class PaymentService
  def initialize(processor)
    @processor = processor
  end
  
  def charge(amount)
    result = @processor.process(amount)
    log_transaction(result)
    result
  end
  
  private
  
  def log_transaction(result)
    # Logging logic
  end
end

# Adding new payment method requires no changes to PaymentService
class CryptoProcessor < PaymentProcessor
  def initialize(wallet_address)
    @wallet_address = wallet_address
  end
  
  def process(amount)
    # Cryptocurrency processing
    { status: :success, transaction_id: crypto_tx_id, amount: amount }
  end
  
  private
  
  def crypto_tx_id
    "0x#{SecureRandom.hex(32)}"
  end
end

Liskov Substitution with proper inheritance:

# Base class establishes contract
class Rectangle
  attr_reader :width, :height
  
  def initialize(width, height)
    @width = width
    @height = height
  end
  
  def area
    width * height
  end
end

# Violation: Square breaks Rectangle contract
class Square < Rectangle
  def initialize(side)
    super(side, side)
  end
  
  # Breaking LSP: modifying width affects height
  def width=(value)
    @width = value
    @height = value  # Unexpected side effect
  end
  
  def height=(value)
    @width = value
    @height = value
  end
end

# This breaks when using Square as Rectangle
def stretch_rectangle(rectangle)
  rectangle.width = rectangle.width + 10
  # Expects height unchanged, but Square modifies both
  rectangle.area  # Unexpected result for Square
end

# Correct approach: separate hierarchies
class Shape
  def area
    raise NotImplementedError
  end
end

class RectangleShape < Shape
  attr_accessor :width, :height
  
  def initialize(width, height)
    @width = width
    @height = height
  end
  
  def area
    width * height
  end
end

class SquareShape < Shape
  attr_accessor :side
  
  def initialize(side)
    @side = side
  end
  
  def area
    side * side
  end
end

Interface Segregation with Ruby modules:

# Fine-grained modules define specific capabilities
module Workable
  def work
    raise NotImplementedError
  end
end

module Eatable
  def eat
    raise NotImplementedError
  end
end

module Sleepable
  def sleep
    raise NotImplementedError
  end
end

# Human worker implements all interfaces
class HumanWorker
  include Workable
  include Eatable
  include Sleepable
  
  def work
    puts "Human working"
  end
  
  def eat
    puts "Human eating"
  end
  
  def sleep
    puts "Human sleeping"
  end
end

# Robot implements only relevant interfaces
class RobotWorker
  include Workable
  
  def work
    puts "Robot working"
  end
  
  # No eat or sleep methods required
end

# Manager depends only on Workable
class WorkManager
  def initialize(workers)
    @workers = workers
  end
  
  def manage_work_day
    @workers.each do |worker|
      worker.work  # Only requires Workable interface
    end
  end
end

Dependency Inversion with dependency injection:

# High-level module defines abstraction
class OrderProcessor
  def initialize(payment_gateway:, inventory_service:, notification_service:)
    @payment_gateway = payment_gateway
    @inventory_service = inventory_service
    @notification_service = notification_service
  end
  
  def process(order)
    return false unless @inventory_service.available?(order.items)
    
    payment_result = @payment_gateway.charge(order.total)
    return false unless payment_result[:status] == :success
    
    @inventory_service.reserve(order.items)
    @notification_service.send_confirmation(order.customer, order)
    
    true
  end
end

# Low-level modules implement abstractions
class StripeGateway
  def charge(amount)
    # Stripe-specific implementation
    { status: :success, transaction_id: "stripe_#{SecureRandom.hex}" }
  end
end

class DatabaseInventory
  def initialize(database)
    @database = database
  end
  
  def available?(items)
    items.all? do |item|
      count = @database.query('SELECT count FROM inventory WHERE id = ?', item.id).first
      count['count'] >= item.quantity
    end
  end
  
  def reserve(items)
    items.each do |item|
      @database.execute('UPDATE inventory SET count = count - ? WHERE id = ?', 
                       item.quantity, item.id)
    end
  end
end

class EmailNotification
  def send_confirmation(customer, order)
    # Email sending logic
    puts "Sending confirmation to #{customer.email}"
  end
end

# Composition root wires dependencies
processor = OrderProcessor.new(
  payment_gateway: StripeGateway.new,
  inventory_service: DatabaseInventory.new(database),
  notification_service: EmailNotification.new
)

Practical Examples

E-commerce Order Processing System:

# Domain model with single responsibility
class Order
  attr_reader :id, :customer_id, :items, :status
  
  def initialize(id:, customer_id:, items:)
    @id = id
    @customer_id = customer_id
    @items = items
    @status = :pending
  end
  
  def total
    items.sum(&:price)
  end
  
  def complete
    @status = :completed
  end
  
  def cancel
    @status = :cancelled
  end
end

# Repository for persistence
class OrderRepository
  def initialize(database)
    @database = database
  end
  
  def save(order)
    @database.transaction do
      save_order(order)
      save_order_items(order)
    end
  end
  
  def find(id)
    row = @database.query('SELECT * FROM orders WHERE id = ?', id).first
    return nil unless row
    
    items = load_order_items(id)
    Order.new(id: row['id'], customer_id: row['customer_id'], items: items)
  end
  
  private
  
  def save_order(order)
    @database.execute(
      'INSERT INTO orders (id, customer_id, status) VALUES (?, ?, ?)',
      order.id, order.customer_id, order.status
    )
  end
  
  def save_order_items(order)
    order.items.each do |item|
      @database.execute(
        'INSERT INTO order_items (order_id, product_id, price) VALUES (?, ?, ?)',
        order.id, item.product_id, item.price
      )
    end
  end
  
  def load_order_items(order_id)
    @database.query('SELECT * FROM order_items WHERE order_id = ?', order_id)
             .map { |row| OrderItem.new(product_id: row['product_id'], 
                                       price: row['price']) }
  end
end

# Open/closed pricing strategy
class PricingStrategy
  def calculate(order)
    raise NotImplementedError
  end
end

class StandardPricing < PricingStrategy
  def calculate(order)
    order.total
  end
end

class BulkDiscountPricing < PricingStrategy
  def initialize(threshold:, discount_percent:)
    @threshold = threshold
    @discount_percent = discount_percent
  end
  
  def calculate(order)
    total = order.total
    return total if total < @threshold
    
    total * (1 - @discount_percent / 100.0)
  end
end

class SeasonalPricing < PricingStrategy
  def initialize(seasonal_discount:)
    @seasonal_discount = seasonal_discount
  end
  
  def calculate(order)
    order.total * (1 - @seasonal_discount / 100.0)
  end
end

# Service applying pricing without knowing concrete strategies
class OrderPricingService
  def initialize(strategy)
    @strategy = strategy
  end
  
  def calculate_final_price(order)
    @strategy.calculate(order)
  end
end

Report Generation System with Interface Segregation:

# Focused interfaces for different concerns
module Exportable
  def export(data)
    raise NotImplementedError
  end
end

module Formattable
  def format(data)
    raise NotImplementedError
  end
end

module Sendable
  def send(content, recipient)
    raise NotImplementedError
  end
end

# Exporters implement only Exportable
class CsvExporter
  include Exportable
  
  def export(data)
    data.map { |row| row.join(',') }.join("\n")
  end
end

class JsonExporter
  include Exportable
  
  def export(data)
    require 'json'
    data.to_json
  end
end

# Formatters implement only Formattable
class TableFormatter
  include Formattable
  
  def format(data)
    # ASCII table formatting
    header = data.first.keys.join(' | ')
    separator = '-' * header.length
    rows = data.map { |row| row.values.join(' | ') }
    
    [header, separator, *rows].join("\n")
  end
end

class SummaryFormatter
  include Formattable
  
  def format(data)
    "Total records: #{data.size}\n" +
    "Fields: #{data.first.keys.join(', ')}"
  end
end

# Senders implement only Sendable
class EmailSender
  include Sendable
  
  def send(content, recipient)
    # Email delivery logic
    puts "Emailing report to #{recipient}"
  end
end

class FileSender
  include Sendable
  
  def send(content, recipient)
    File.write(recipient, content)
    puts "Saved report to #{recipient}"
  end
end

# Report generator composes focused dependencies
class ReportGenerator
  def initialize(exporter:, formatter:, sender:)
    @exporter = exporter
    @formatter = formatter
    @sender = sender
  end
  
  def generate(data, destination)
    exported = @exporter.export(data)
    formatted = @formatter.format(data)
    @sender.send(formatted, destination)
  end
end

Notification System Demonstrating Multiple Principles:

# Abstract notification defines contract (OCP)
class NotificationChannel
  def deliver(message, recipient)
    raise NotImplementedError
  end
  
  def available?
    true
  end
end

# Concrete channels extend without modifying base (OCP)
class EmailChannel < NotificationChannel
  def initialize(smtp_config)
    @smtp_config = smtp_config
  end
  
  def deliver(message, recipient)
    # Email delivery via SMTP
    send_email(recipient.email, message.subject, message.body)
  end
  
  def available?
    @smtp_config.valid?
  end
  
  private
  
  def send_email(to, subject, body)
    # SMTP implementation
  end
end

class SmsChannel < NotificationChannel
  def initialize(sms_gateway)
    @sms_gateway = sms_gateway
  end
  
  def deliver(message, recipient)
    @sms_gateway.send_sms(
      to: recipient.phone,
      message: "#{message.subject}: #{message.body}"
    )
  end
  
  def available?
    @sms_gateway.connected?
  end
end

class PushChannel < NotificationChannel
  def initialize(push_service)
    @push_service = push_service
  end
  
  def deliver(message, recipient)
    @push_service.send_notification(
      device_token: recipient.device_token,
      title: message.subject,
      body: message.body
    )
  end
end

# Notification message (SRP)
class NotificationMessage
  attr_reader :subject, :body, :priority
  
  def initialize(subject:, body:, priority: :normal)
    @subject = subject
    @body = body
    @priority = priority
  end
end

# Recipient information (SRP)
class NotificationRecipient
  attr_reader :email, :phone, :device_token, :preferences
  
  def initialize(email:, phone: nil, device_token: nil, preferences: {})
    @email = email
    @phone = phone
    @device_token = device_token
    @preferences = preferences
  end
  
  def prefers?(channel_type)
    preferences.fetch(channel_type, true)
  end
end

# Notification service depends on abstractions (DIP)
class NotificationService
  def initialize(channels, logger)
    @channels = channels
    @logger = logger
  end
  
  def notify(message, recipient)
    delivered = false
    
    @channels.each do |channel|
      next unless channel.available?
      
      begin
        channel.deliver(message, recipient)
        @logger.log("Delivered via #{channel.class.name}")
        delivered = true
        break
      rescue => e
        @logger.log("Failed via #{channel.class.name}: #{e.message}")
      end
    end
    
    raise DeliveryError, "All channels failed" unless delivered
  end
end

Design Considerations

SOLID principles guide design decisions but do not provide absolute rules for every situation. Applying these principles requires balancing multiple concerns including development velocity, system complexity, team experience, and domain requirements. Over-application creates unnecessary abstraction layers while under-application produces rigid, tightly coupled systems.

When to Apply Single Responsibility:

Classes naturally accumulate multiple responsibilities during rapid prototyping or when requirements remain unclear. Extracting responsibilities too early creates premature abstractions that resist changing requirements. Apply SRP when a class exhibits multiple independent reasons to change or when different team members modify the same class for unrelated reasons.

Signs indicating SRP violations include classes with many dependencies, methods unrelated to the class's primary purpose, and tests requiring extensive mocking. A UserController that handles HTTP requests, validates input, performs business logic, updates databases, and sends notifications violates SRP. Each responsibility represents a different rate of change and reason for modification.

Applying SRP has costs. More classes increase cognitive load when navigating a codebase. Finding where specific functionality resides requires understanding how responsibilities distribute across classes. Balance specificity with discoverability. A system with 200 micro-classes becomes as difficult to maintain as one with 10 god classes.

When to Apply Open/Closed:

Not every class needs extensibility. Utility classes performing fixed algorithms or value objects representing immutable data rarely require extension. Apply OCP to variation points where requirements indicate multiple implementations of similar functionality or where future feature additions follow predictable patterns.

Payment processing, notification delivery, report generation, and pricing calculations represent common variation points. These domains naturally accommodate multiple implementations of a core concept. Applying OCP upfront in these areas prevents modification cascades when adding variants.

Premature application creates complexity. Abstracting a single concrete implementation because "we might need variants later" adds indirection without immediate benefit. Wait until a second implementation emerges to extract the abstraction. The first concrete implementation clarifies what needs abstraction.

When to Apply Liskov Substitution:

LSP violations often emerge from misunderstanding inheritance relationships. Mathematical or linguistic relationships do not guarantee behavioral substitutability. Design inheritance hierarchies based on behavioral contracts rather than conceptual relationships.

Prefer composition over inheritance when subtypes cannot maintain supertype contracts. A Stack and Queue both store collections but have incompatible access patterns. Inheriting both from Collection creates LSP violations if Collection defines methods incompatible with stack or queue semantics.

Testing reveals LSP violations. Tests written for a supertype should pass when using any subtype instance. If tests require special cases for specific subtypes, those subtypes violate LSP. This indicates the abstraction does not accurately capture the behavioral contract.

When to Apply Interface Segregation:

Ruby's duck typing reduces ISP importance compared to statically typed languages. Classes implement only methods they use without declaring interface conformance. However, ISP remains relevant for module design and API boundaries.

Large modules combining multiple responsibilities force including classes to provide all methods even when using only a subset. Split modules when different clients use different subsets of functionality. A Persistence module combining save, query, transaction, and migration methods forces including classes to implement all methods.

API design benefits significantly from ISP. Public interfaces exposing many methods create backward compatibility challenges. Clients depend on entire interfaces, making any change potentially breaking. Focused interfaces minimize coupling between API providers and consumers.

When to Apply Dependency Inversion:

Application of DIP concentrates in areas requiring flexibility for testing, deployment environments, or business rule changes. Core business logic, use case orchestrators, and domain services benefit most from depending on abstractions rather than concrete implementations.

Infrastructure details like databases, file systems, external APIs, and framework code represent the "details" in DIP. Business logic defines interfaces describing what it needs from infrastructure without depending on specific implementations. This inverts dependencies so business logic remains stable while infrastructure varies.

Ruby's dynamic nature allows DIP through duck typing without explicit interface declarations. Any object responding to required methods satisfies the abstraction. This reduces boilerplate compared to statically typed languages but requires discipline to maintain clear contracts.

Common Pitfalls

Over-Engineering with Premature Abstraction:

Developers familiar with SOLID principles sometimes apply them prematurely, creating abstraction layers before understanding actual requirements. A single EmailSender class becomes NotificationStrategy, NotificationChannel, NotificationFactory, and multiple concrete implementations before sending a single email. This abstraction premature increases complexity without providing value.

# Over-engineered for current needs
class NotificationStrategyFactory
  def create(type)
    case type
    when :email
      EmailNotificationStrategy.new(
        EmailChannelAdapter.new(
          SmtpConfiguration.instance
        )
      )
    end
  end
end

# Simpler sufficient solution
class EmailSender
  def send(to, subject, body)
    # Send email
  end
end

Wait for actual requirements before creating abstractions. When only one implementation exists and no planned variations emerge, concrete classes suffice. Refactor toward abstractions when the second implementation appears.

Violating Liskov Through Partial Implementation:

Subclasses that throw errors for inherited methods violate LSP even when those methods seem inappropriate. Inheritance creates an "is-a" relationship implying full behavioral compatibility. Subclasses throwing NotImplementedError for inherited methods break this contract.

# LSP violation through partial implementation
class Bird
  def fly
    raise NotImplementedError
  end
  
  def eat
    "Bird eating"
  end
end

class Penguin < Bird
  def fly
    raise "Penguins can't fly!"  # Violates LSP
  end
  
  def eat
    "Penguin eating fish"
  end
end

def bird_behavior(bird)
  bird.fly  # Breaks with Penguin
  bird.eat
end

Redesign hierarchies to avoid partial implementations. Extract flying behavior into a separate module or redesign the abstraction to represent capabilities shared by all subtypes.

Creating Anemic Domain Models:

Excessive focus on SRP sometimes produces domain models containing only data with no behavior. All logic migrates to service classes while domain objects become data containers. This violates object-oriented principles and creates procedural code disguised as objects.

# Anemic domain model
class Order
  attr_accessor :items, :status, :total
end

class OrderService
  def calculate_total(order)
    order.total = order.items.sum(&:price)
  end
  
  def validate(order)
    order.items.any? && order.total > 0
  end
  
  def complete(order)
    order.status = :completed
  end
end

# Better: behavior lives with data
class Order
  def calculate_total
    @total = items.sum(&:price)
  end
  
  def valid?
    items.any? && total > 0
  end
  
  def complete
    @status = :completed
  end
end

Domain models should contain business rules related to their data. Services orchestrate interactions between domain objects but should not contain logic that belongs in domain models.

Dependency Injection Becoming Service Locator:

Passing too many dependencies through constructors leads to service locator patterns disguised as dependency injection. Classes receive a configuration object or dependency container from which they extract needed dependencies. This hides dependencies and creates coupling to the container.

# Service locator anti-pattern
class OrderProcessor
  def initialize(container)
    @container = container
  end
  
  def process(order)
    # Hidden dependencies
    payment = @container.get(:payment_gateway)
    inventory = @container.get(:inventory_service)
    notifier = @container.get(:notification_service)
    
    # Processing logic
  end
end

# Explicit dependency injection
class OrderProcessor
  def initialize(payment_gateway:, inventory_service:, notification_service:)
    @payment_gateway = payment_gateway
    @inventory_service = inventory_service
    @notification_service = notification_service
  end
  
  def process(order)
    # Processing logic with explicit dependencies
  end
end

Explicit constructor injection makes dependencies visible and testable. Long parameter lists indicate the class has too many responsibilities requiring further decomposition.

Confusing Single Responsibility with Single Method:

SRP does not mandate that classes have only one public method or handle only one operation. A class has single responsibility when all its methods relate to a coherent purpose. A User class validating email format, checking name presence, and comparing passwords all relate to the single responsibility of managing user data integrity.

# Misunderstanding SRP
class UserEmailValidator
  def validate(email)
    # Email validation
  end
end

class UserNameValidator
  def validate(name)
    # Name validation
  end
end

# Appropriate grouping
class User
  def valid?
    email_valid? && name_valid?
  end
  
  private
  
  def email_valid?
    # Email validation
  end
  
  def name_valid?
    # Name validation
  end
end

Related validations belong together in the domain model. Splitting every method into a separate class creates fragmentation without improving maintainability.

Reference

SOLID Principles Summary

Principle Description Key Benefit Primary Indicator
Single Responsibility Class has one reason to change Reduces coupling between concerns Multiple unrelated methods or dependencies
Open/Closed Open to extension, closed to modification Enables feature addition without changing existing code Conditional logic for variants
Liskov Substitution Subtypes must be substitutable for supertypes Maintains polymorphic behavior Subtype-specific handling in client code
Interface Segregation Clients depend only on methods they use Reduces coupling from unused methods Large interfaces with unrelated methods
Dependency Inversion Depend on abstractions not concretions Enables flexibility and testability Direct instantiation of concrete classes

Principle Violations and Solutions

Violation Symptom Solution
God class Class handles multiple unrelated concerns Extract responsibilities into focused classes
Tight coupling Changes cascade through multiple classes Introduce abstractions and dependency injection
Conditional complexity Many conditionals for type checking Apply polymorphism with strategy pattern
Interface pollution Implementing empty methods Split interface into focused contracts
Rigid dependencies Cannot swap implementations Inject dependencies through constructors

Ruby-Specific SOLID Approaches

Principle Ruby Feature Implementation Pattern
SRP Classes and Modules Compose behavior using include and extend
OCP Inheritance and Modules Define base contracts, extend with subclasses
LSP Duck Typing Design behavioral contracts, test substitutability
ISP Selective Module Inclusion Include only required modules per class
DIP Constructor Parameters Pass dependencies as keyword arguments

Common Refactoring Patterns

From To Principle Applied
Large class with multiple concerns Multiple focused classes SRP
Conditional logic for variants Polymorphic classes with common interface OCP
Type checking in client code Subtypes maintaining contracts LSP
Large module with unrelated methods Multiple focused modules ISP
Direct instantiation of concrete classes Dependency injection with abstractions DIP

Design Decision Matrix

Scenario Apply SOLID Defer SOLID Rationale
Multiple implementations exist Yes No Clear variation point
Single implementation, no variants planned No Yes Avoid premature abstraction
Core business logic Yes No Needs flexibility and testing
Utility functions No Yes Fixed behavior, rarely changes
External API integration points Yes No Enable swapping implementations
Framework initialization code No Yes One-time setup, rarely changes
Domain models Yes No Business rules require clear design
Script or prototype code No Yes Optimize for speed, refactor later