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 |