skills/ruby-object-design/SKILL.md
Automatically invoked when making decisions about Ruby code structure and organization. Triggers on "class or module", "should this be a class", "struct vs class", "PORO", "data object", "design pattern", "class vs module", "when to use class", "module vs class", "stateless class", "value object", "data container", "object factory", "extend self", "singleton class". Provides guidance on choosing the right Ruby construct (class, module, Struct, Data, Hash). NOT for code smell identification or refactoring (use ruby-refactoring) or Rails-specific framework patterns.
npx skillsauth add ag0os/rails-dev-plugin ruby-object-designInstall 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.
Ruby is object-oriented, not class-oriented. The class keyword should be reserved for specific use cases, not used as a default container for code.
Only use class if you are creating an object factory -- a template for generating multiple objects that encapsulate internal state with behaviors operating on that state.
If your code doesn't create multiple instances with distinct state, a class is the wrong construct.
Do you need multiple instances with encapsulated state?
|-- YES: Does the object have both state AND behavior?
| |-- YES -> Class
| +-- NO (just data) -> Struct or Data
+-- NO: Is this a collection of related functions?
|-- YES -> Module with `extend self`
+-- NO: Is this a one-off transformation?
+-- YES -> standalone method or lambda
If your class has no instance variables, it's a module pretending to be a class.
# Bad: Class with no state
class StringUtils
def self.titleize(string) = string.split.map(&:capitalize).join(' ')
end
# Good: Module with extend self
module StringUtils
extend self
def titleize(string) = string.split.map(&:capitalize).join(' ')
end
Why module: Clearer intent, can be included, no misleading .new method.
Classes with only initialize + call are often functions in disguise. Ask:
initialize + call adding clarity or ceremony?# Questionable: function in a class costume
class CalculateDiscount
def initialize(order) = @order = order
def call = @order.subtotal * discount_rate
private
def discount_rate = @order.customer.premium? ? 0.1 : 0.05
end
# Alternative: Module function
module Discounts
extend self
def calculate(order) = order.subtotal * discount_rate(order.customer)
private
def discount_rate(customer) = customer.premium? ? 0.1 : 0.05
end
Exception: Service classes ARE appropriate when the project is on the extracted Axis A value (rails-stack-profiles) and uses service objects consistently. Don't fight the codebase convention.
If your class is named Factory, Builder, Decorator, Adapter, or AbstractBase, reconsider. Most GoF patterns were workarounds for C++/Java limitations and are unnecessary in Ruby.
See class-vs-module.md for Ruby-native alternatives.
If an object requires calling setters before it functions, fix the constructor:
# Bad: requires setup ceremony after .new
report = ReportGenerator.new
report.set_data(data) # Must call before generate!
report.generate
# Good: valid at birth
ReportGenerator.new(data: data).generate
Prefer Data.define over classes for immutable value objects. It provides ==, hash, with, and keyword-only new out of the box.
Point = Data.define(:x, :y) do
def distance_from_origin = Math.sqrt(x**2 + y**2)
def translate(dx, dy) = with(x: x + dx, y: y + dy) # returns new instance
end
Pre-3.2 fallback: Use frozen Struct with keyword_init: true.
See data-structures.md for the full graduation path: Hash -> Struct -> Data -> Class.
| Scenario | Use | Why |
|----------|-----|-----|
| Multiple instances with state + behavior | Class | True object factory |
| Stateless utility methods | Module with extend self | No state to encapsulate |
| Simple data container | Struct or Data | Avoids boilerplate |
| Immutable value object | Data (3.2+) or frozen Struct | Built-in immutability |
| Ad-hoc/temporary data | Hash | Simplest solution |
| Named after a design pattern | Rethink design | Patterns often unnecessary in Ruby |
| Invalid after .new without setup | Not a class | Objects must be valid at birth |
Before applying these principles, check the existing codebase:
Data.define requires 3.2+grep -r "class.*Service" app/ to understand the project's styleWhen providing object design recommendations:
development
WHAT: Language-agnostic corrective guidance for the refactoring phase. WHEN: Agent is restructuring code, fixing code smells, reducing complexity, or improving maintainability. NOT FOR: Writing new features, debugging runtime errors, performance tuning, or object design decisions.
tools
Analyzes Rails view templates, partials, layouts, helpers, and form patterns for best practices. Use when reviewing ERB templates, improving view performance with fragment caching, fixing form helpers, organizing partials, adding accessibility attributes, or evaluating collection rendering. NOT for Stimulus/Turbo logic (use hotwire-patterns), controller concerns, or API-only responses.
testing
Analyzes Rails test suites and recommends testing best practices for RSpec and Minitest. Use when writing new tests, reviewing test coverage, fixing flaky tests, improving test performance, choosing between test types (unit, integration, system, request), or setting up factories and fixtures. NOT for production monitoring, deployment verification, or load/stress testing infrastructure.
development
Detects a Rails project's architecture axes — logic placement (native vs extracted) and delivery (html vs api) — so other skills load profile-appropriate guidance without inline conditionals. Use when planning architecture or when a recommendation depends on where business logic lives or whether the app renders HTML or serves JSON. NOT for test framework, job backend, cache store, or auth library choices — those are orthogonal facts detected by project-conventions.