| name | simplifying-ruby-code |
| description | Identify over-engineering in Ruby - prefer simple data structures (Hash, Struct, Data) and pure functions over unnecessary classes |
Simplifying Ruby Code
Core Principle
Prefer simple data structures (Hash, Array, Struct, Data) and pure functions over unnecessary classes and abstractions.
MANDATORY: Identify whether code is a decision (pure logic) or effect (I/O). Keep them separate. See writing-code skill.
When to Use
- Command objects with single
callmethod and no state - Value objects that just wrap data without behavior
- Service classes that could be module functions
- Custom classes for simple data (coordinates, ranges, tuples)
- Deep inheritance where composition would work
- Missing Ruby protocols (
to_h,to_a,each) - Tests require extensive mocking (indicates mixed concerns)
Over-Engineering Patterns
Command Objects → Module Functions
# ❌ Over-engineered
class UserCreator
def initialize(params); @params = params; end
def call; User.create(@params); end
end
# ✅ Simple
User.create(params) # or module function if logic needed
Keep command object when: Has state, multi-step algorithm, needs queuing.
Value Objects → Struct/Data/Hash
# ❌ Manual value object
class Point
attr_reader :x, :y
def initialize(x, y); @x, @y = x, y; end
def ==(other); x == other.x && y == other.y; end
end
# ✅ Simple
Point = Data.define(:x, :y) # Ruby 3.2+, immutable
Point = Struct.new(:x, :y, keyword_init: true) # mutable
point = {x: 10, y: 20} # simplest
Utility Classes → Modules
# ❌ Class with only class methods
class DateFormatter
def self.format_for_display(date); date.strftime("%B %d, %Y"); end
end
# ✅ Module
module DateFormatter
module_function
def format_for_display(date); date.strftime("%B %d, %Y"); end
end
Deep Inheritance → Composition
# ❌ Deep hierarchy
class Animal; end
class Mammal < Animal; end
class Dog < Mammal; end
# ✅ Composition
module WarmBlooded
def warm_blooded?; true; end
end
class Dog
include WarmBlooded
end
Data Structure Selection
| Use | When |
|---|---|
| Hash | Temporary data, varying keys, JSON interface |
| Struct | Fixed attributes, need methods, mutable OK |
| Data | Fixed attributes, immutable (Ruby 3.2+) |
| Custom Class | Complex validation, rich behavior, domain concepts |
Ruby Protocols
Implement for interoperability with standard library:
class Collection
include Enumerable
def each(&block); @items.each(&block); end # Enables map, select, etc.
def to_a; @items.dup; end
def to_h; @items.to_h; end
def to_json(*args); @items.to_json(*args); end
end
Key protocols: to_h, to_a, to_json, to_s, each, <=>, hash/eql?
Refactoring Steps
- Identify decisions vs effects - Mark pure logic vs I/O
- Extract pure functions - Create module functions with data parameters
- Test pure functions - No mocks needed
- Simplify data structures - Replace classes with Struct/Data/Hash
- Remove unnecessary layers - Inline wrappers that add no value
Detection Checklist
- Command object with single method, no state → Module function
- Value object with no behavior → Struct/Data/Hash
- Class with only class methods → Module
- Service wrapping single operation → Direct call
- Deep inheritance for behavior sharing → Modules/composition
- Missing
to_h,to_a,to_json→ Add protocols - Tests need heavy mocking → Separate decisions from effects
Key Takeaways
- Hash/Struct/Data over custom classes for simple data
- Module functions over command objects unless state needed
- Composition over inheritance for behavior sharing
- Implement Ruby protocols for interoperability
- OOP for domain models, functional for calculations - Use both appropriately