writing-affordances
Use when refactoring fat Rails models with repetitive prefixes like entropy_*, notification_*, or multi-association coordination. Extracts PORO wrappers that group operations around nouns/concepts.
When & Why to Use This Skill
This Claude skill automates the refactoring of 'fat' Ruby on Rails models by extracting complex logic into Plain Old Ruby Object (PORO) wrappers known as 'Affordances.' It helps developers identify method clusters with repetitive prefixes and reorganize them into clean, conceptual domains, significantly improving code maintainability, readability, and adherence to clean architecture principles in Rails applications.
Use Cases
- Refactoring Rails models where three or more methods share a common prefix (e.g., entropy_*) to group them into a dedicated conceptual object.
- Simplifying complex models that coordinate multiple associations by extracting the coordination logic into a standalone PORO wrapper.
- Improving code readability and API design by converting flat, prefixed method calls into nested, domain-driven interfaces (e.g., card.entropy.calculate).
- Systematically separating implicit Rails behaviors like callbacks and validations (Concerns) from explicit domain operations (Affordances) to reduce side effects.
| name | writing-affordances |
|---|---|
| description | Use when refactoring fat Rails models with repetitive prefixes like entropy_*, notification_*, or multi-association coordination. Extracts PORO wrappers that group operations around nouns/concepts. |
Writing Affordances
Extract PORO wrappers that group 3+ operations around a noun/concept, keeping parent model focused.
Announce: "I'm using the writing-affordances skill to evaluate this for affordance extraction."
Detection Triggers
- 3+ methods share prefix:
entropy_calculate,entropy_remind,entropy_cleanup - Methods coordinate 2+ associations (
assignments+watches+events) - Fat model with mixed conceptual domains
- Related methods but connection non-obvious
Don't use for: Single trait (use concern) | Single association (use extension) | 1-2 methods (use plain method)
Decision Heuristics
Ask yourself:
- "Can I pass this around?" - Would
notify(description)be clearer thannotify(event)? - "Do I call it or does Rails?" - Explicit calls = Affordance | Callbacks/validations = Concern
- "Prefix cluster?" -
entropy_*methods = Extractentropyaffordance (prefix = noun) - "Call site clarity?" -
card.entropy.auto_clean_atvscard.entropy_auto_clean_at - "Need infrastructure + operations?" - Concern provides associations/callbacks, affordance provides API
See @detecting.md for complete decision framework.
Pattern Comparison
| Pattern | Use When | Example | Key Question |
|---|---|---|---|
| Affordance | Group operations around noun | card.entropy.auto_clean_at |
"Can I pass this around?" |
| Concern | Infrastructure or implicit behavior | Searchable (callbacks only) |
"Does Rails call it implicitly?" |
| Concern + Affordance | Both infrastructure AND operations | Concern sets up associations, affordance provides API | "Need both?" |
| Association Ext | Single association operations | card.comments.recent |
"Only touching one has_many?" |
Critical Constraints
Violating these = wrong pattern:
- 3+ methods minimum - Don't create single-method affordances
- PORO only - No ActiveRecord inheritance
- Parent is ONLY dependency -
Card::Entropy.new(card), notnew(card, user) - Don't memoize parameterized -
entropy(as_of: date)should NOT use||=
Quick Pattern
# Before: Methods scattered in model or prefix smell
card.entropy_auto_clean_at
card.entropy_days_before_reminder
card.entropy_reminder_sent?
# After: Affordance
card.entropy.auto_clean_at
card.entropy.days_before_reminder
card.entropy.reminder_sent?
# Entry point in concern
module Card::Entropic
def entropy
Card::Entropy.for(self) # Factory pattern
end
def entropic?
entropy.present?
end
end
# PORO affordance class
class Card::Entropy
attr_reader :card, :auto_clean_period
class << self
def for(card)
return unless card.last_active_at
new(card, card.auto_postpone_period)
end
end
def initialize(card, auto_clean_period)
@card = card
@auto_clean_period = auto_clean_period
end
def auto_clean_at
card.last_active_at + auto_clean_period
end
def days_before_reminder
(auto_clean_period * 0.25).seconds.in_days.round
end
end
Verification
Non-obvious checks before completion:
- 3+ related methods (not single-method wrapper)
- Parameterized affordances NOT memoized
- Class documentation with usage examples
- Tests: both isolated and integration
Sub-documents
- @detecting.md - Full decision framework with clarifying questions
- @implementing.md - File organization, naming, concerns + affordances patterns