“Clean code is its own best documentation.” — Steve McConnell
When we start programming, it's common to focus on just making it work. But true skill lies in making it maintainable, readable, and easy to extend over time.
That's where the SOLID principles come in. In this article, I’ll show you what they are, why they matter, and how to apply them with a real Ruby example to see how powerful they can be.
🧱 What is SOLID?
SOLID is a set of five object-oriented design principles introduced by Robert C. Martin to improve code quality and make it scalable and maintainable.
The Principles:
- S - Single Responsibility Principle (SRP)
- O - Open/Closed Principle (OCP)
- L - Liskov Substitution Principle (LSP)
- I - Interface Segregation Principle (ISP)
- D - Dependency Inversion Principle (DIP)
SRP - Single Responsibility Principle: A class should have only one reason to change — a single responsibility.
OCP - Open/Closed Principle: Software should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.
LSP - Liskov Substitution Principle: Subclasses should be substitutable for their base classes without altering the correctness of the program.
ISP - Interface Segregation Principle: Clients should not be forced to depend on interfaces they do not use. Prefer small, specific interfaces over large ones.
DIP - Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions.
📚 Case Study: Report Generator
Let’s imagine we are building an app that generates reports in multiple formats. We start with PDF and HTML, but then Markdown and DOCX get added.
Let’s see how to apply SOLID to this scenario.
❌ Version 1: Rigid and Hard to Scale
class Report
attr_reader :title, :content
def initialize(title, content)
@title = title
@content = content
end
def generate_pdf
puts "Generating PDF for #{@title}"
end
def generate_html
puts "Generating HTML for #{@title}"
end
end
This version works, but it violates two major principles:
SRP: Report has two responsibilities: storing data and generating formats.
OCP: To add a new format (e.g. DOCX), you must modify this class.
✅ Refactored: SOLID Principles Applied
# SRP (Single Responsibility Principle):
# This class is only responsible for storing the report's data.
class Report
attr_reader :title, :content
def initialize(title, content)
@title = title
@content = content
end
end
# DIP (Dependency Inversion Principle):
# Abstract class that defines an interface for formatters.
# High-level classes will depend on this abstraction.
class ReportFormatter
def format(report)
raise NotImplementedError, 'This method must be implemented'
end
end
# SRP and LSP
class PDFFormatter < ReportFormatter
def format(report)
puts "Generating PDF for #{report.title}"
end
end
# SRP and LSP
class HTMLFormatter < ReportFormatter
def format(report)
puts "Generating HTML for #{report.title}"
end
end
# OCP, SRP, LSP
class MarkdownFormatter < ReportFormatter
def format(report)
puts "# #{report.title}\n\n#{report.content}"
end
end
# Client using the system
# DIP: Does not depend on a specific class, only on the abstraction
report = Report.new("Sales Report", "This is the content of the report")
formatter = HTMLFormatter.new
formatter.format(report)
formatter = PDFFormatter.new
formatter.format(report)
formatter = MarkdownFormatter.new
formatter.format(report)
What did we improve?
SRP: Report only handles data, not formatting.
OCP: We can add MarkdownFormatter without modifying any existing classes.ninguna clase existente.
LSP: Any formatter can be used as a ReportFormatter without breaking the system.
DIP: The client depends on the abstraction (ReportFormatter), not on specific implementations.