ai_misc/skills/solid-principles/SKILL.md
Apply SOLID principles when writing, reviewing, or refactoring Ruby code. This skill should be used when designing classes, evaluating architecture, reviewing pull requests, or refactoring existing code. It provides actionable checklists, violation detection patterns, and Ruby-idiomatic refactoring strategies for each of the five SOLID principles.
npx skillsauth add madbomber/experiments solid-principlesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Apply the five SOLID principles as practical tools for writing, reviewing, and refactoring Ruby code. Each principle includes detection heuristics, Ruby-idiomatic solutions, and concrete examples.
| Principle | One-line rule | Violation smell |
|---|---|---|
| S — Single Responsibility | A class has only one reason to change | Class name needs "And" or "Manager" |
| O — Open/Closed | Extend behavior without modifying existing code | Case statements that grow with new types |
| L — Liskov Substitution | Subclasses substitute for their parent without surprises | Subclass raises NotImplementedError or changes return type |
| I — Interface Segregation | Depend only on methods you actually call | Including a module but using 2 of its 15 methods |
| D — Dependency Inversion | Depend on abstractions, not concretions | Hard-coded class names inside methods |
Rule: A class should have only one reason to change.
# VIOLATION: Order handles persistence, formatting, AND notification
class Order
def save
DB.insert(to_hash)
end
def to_pdf
PDFGenerator.new(self).render
end
def send_confirmation
Mailer.deliver(confirmation_email)
end
end
Extract each responsibility into its own object:
# Each class has one reason to change
class Order
attr_reader :items, :total
# Domain logic only: what IS an order?
end
class OrderRepository
def save(order) = DB.insert(order.to_hash)
end
class OrderPresenter
def initialize(order) = @order = order
def to_pdf = PDFGenerator.new(@order).render
end
class OrderNotifier
def initialize(order) = @order = order
def send_confirmation = Mailer.deliver(confirmation_email)
end
Single responsibility does not mean single method. A class that groups
closely related behavior is fine. User having full_name, email_domain,
and active? is one responsibility: representing a user.
Rule: Open for extension, closed for modification.
case/if-elsif chainwhen :new_thing to growing switch statements# VIOLATION: Adding a new format requires modifying this method
def export(format)
case format
when :csv then export_csv
when :json then export_json
when :xml then export_xml
# Adding :pdf means editing this method
end
end
Use polymorphism, a registry, or Ruby's duck typing:
# Strategy pattern with registry
module Exporters
REGISTRY = {}
def self.register(format, klass)
REGISTRY[format] = klass
end
def self.for(format)
REGISTRY.fetch(format) { raise ArgumentError, "Unknown format: #{format}" }
end
end
class CsvExporter
def export(data) = # ...
end
Exporters.register(:csv, CsvExporter)
# Adding :pdf requires only a new class + registration — no existing code changes
def export(format)
Exporters.for(format).new.export(data)
end
Ruby-idiomatic alternatives:
def self.inherited hook for auto-registrationdef self.included callback"Exporters::#{format.to_s.classify}".constantizeRule: Objects of a subclass must be substitutable for objects of their superclass without altering correctness. A dependency can be swapped out so long as the actual and expected contracts are met — the new dependency requires NO MORE and promises NO LESS.
NotImplementedError for an inherited methodis_a? or class to decide behavior# VIOLATION: Square breaks Rectangle's contract
class Rectangle
attr_accessor :width, :height
def area = width * height
end
class Square < Rectangle
def width=(val)
@width = val
@height = val # Surprise! Setting width changes height
end
end
# Client code breaks:
def stretch(rect)
rect.width = 10
rect.height = 5
rect.area # expects 50, Square returns 100
end
Ruby's duck typing is a powerful LSP tool. Informal contracts between a client and a dependency mean any object that quacks correctly can be swapped in:
# Client depends on the contract: responds to #call with (message)
# and returns a truthy/falsy delivery status
class NotificationSender
def initialize(transport:)
@transport = transport
end
def send(message)
@transport.call(message)
end
end
# All of these satisfy the contract:
EmailTransport = ->(msg) { Mailer.deliver(msg) }
SmsTransport = ->(msg) { TwilioClient.send(msg) }
SlackTransport = ->(msg) { SlackWebhook.post(msg) }
NullTransport = ->(_msg) { true } # For testing
# LSP satisfied: any transport substitutes without surprises
Write tests against the interface, not the implementation. If a test suite passes with any conforming object, LSP is satisfied:
# Shared examples enforce the contract
RSpec.shared_examples "a transport" do
it "responds to #call" do
expect(subject).to respond_to(:call)
end
it "accepts a message argument" do
expect { subject.call("test") }.not_to raise_error
end
end
Rule: Depend only on the methods you actually use. Prefer small, focused interfaces over large, general-purpose ones.
# VIOLATION: Reportable is a grab-bag of unrelated methods
module Reportable
def to_csv = # ...
def to_pdf = # ...
def to_json = # ...
def to_xml = # ...
def email_report = # ...
def schedule_report = # ...
def archive_report = # ...
end
# Invoice only needs CSV and PDF but gets everything
class Invoice
include Reportable
end
Split into focused modules:
module CsvExportable
def to_csv = # ...
end
module PdfExportable
def to_pdf = # ...
end
module Schedulable
def schedule = # ...
def archive = # ...
end
class Invoice
include CsvExportable
include PdfExportable
# Only what Invoice actually needs
end
Ruby doesn't have formal interfaces, but the principle still applies:
class Dashboard
extend Forwardable
def_delegators :@report, :total_revenue, :user_count
# Only delegates what Dashboard needs, not all of Report
end
Rule: High-level modules should not depend on low-level modules. Both should depend on abstractions.
SomeSpecificClass.new rather than receiving a collaborator# VIOLATION: OrderProcessor is welded to Stripe and Postgres
class OrderProcessor
def process(order)
payment = Stripe::Charge.create(amount: order.total)
ActiveRecord::Base.connection.execute("INSERT INTO orders ...")
SendGridMailer.deliver(order.confirmation_email)
end
end
Inject dependencies — let the caller decide which implementations to use:
class OrderProcessor
def initialize(payment_gateway:, repository:, notifier:)
@payment_gateway = payment_gateway
@repository = repository
@notifier = notifier
end
def process(order)
@payment_gateway.charge(order.total)
@repository.save(order)
@notifier.confirm(order)
end
end
# Production wiring
OrderProcessor.new(
payment_gateway: StripeGateway.new,
repository: PostgresOrderRepository.new,
notifier: EmailNotifier.new
)
# Test wiring
OrderProcessor.new(
payment_gateway: FakeGateway.new,
repository: InMemoryRepository.new,
notifier: NullNotifier.new
)
# 1. Constructor injection (preferred for required deps)
def initialize(logger: Logger.new($stdout))
@logger = logger
end
# 2. Default with override (good for optional deps)
def initialize(client: nil)
@client = client || DefaultClient.new
end
# 3. Module-level default (Andrew Kane pattern)
module GemName
class << self
attr_writer :client
def client
@client ||= DefaultClient.new
end
end
end
# 4. Block injection (for one-off customization)
def process(&on_error)
rescue StandardError => e
on_error ? on_error.call(e) : raise
end
end
When reviewing code, work through this checklist:
SOLID principles are guidelines, not laws. Accept violations when:
For detailed Ruby examples and edge cases, see:
data-ai
Postgres performance optimization and best practices from Supabase. Use this skill when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations.
tools
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
development
Analyze Ruby and Rails code quality with RubyCritic. Identifies code smells, complexity issues, and refactoring opportunities. Provides detailed metrics, scores files A-F, compares branches, and prioritizes high-churn problem areas. Use when analyzing Ruby code quality, reviewing PRs, or identifying technical debt.
tools
Use at session start for Ruby projects (Gemfile, .ruby-version, or .tool-versions present). Detect version manager BEFORE running ruby, bundle, gem, rake, rails, rspec, or any Ruby command.