def next_after(book) = …
def first_books = … def unlocked_for(student) = … def weeks_for(book) = …
def final_books = … Curriculum is playing, and a candidate for its own object.
The problem
Classes grow by accretion. Curriculum started with one job and now has five public methods. Each
caller only needs a couple of them, but every caller depends on all five, every double of it has to
decide which to stub, and every change to it touches everyone.
That’s the interface segregation principle: no client should depend on methods it doesn’t use. It’s easy to state and hard to see, because the split isn’t in the class. It’s in its callers.
The fix
In your editor, the class name gets a hint:
class Curriculum
def next_after(book) = sequence[book]
def first_books = @first_books
def unlocked_for(student) = eligible(student)
def weeks_for(book) = @weeks[book]
def final_books = @final_books
end
💡 runtime-analysis · hint
Across your suite, Curriculum's callers split into 3 groups.
No caller uses methods from more than one:
Scheduler → { next_after, first_books }
Enrollment → { unlocked_for }
Report → { weeks_for, final_books }
💡 Quick Fix · Extract 3 role interfaces
On the command line, the same report:
$ ra split_interface Curriculum
3 disjoint roles · 0 callers span more than one
Sequencing next_after, first_books ← Scheduler
Unlocking unlocked_for ← Enrollment
Reporting weeks_for, final_books ← Report
The quick-fix extracts each role and points each caller at the one it uses:
class Curriculum
def sequencing = Sequencing.new(self)
def unlocking = Unlocking.new(self)
def reporting = Reporting.new(self)
end
Scheduler.new(curriculum.sequencing)
Enrollment.new(curriculum: curriculum.unlocking)
Now a change to reporting can’t reach Enrollment, and a double for Enrollment’s spec has one
method to stub instead of five.
In a test-first loop this lands at the refactor step. The tests you just wrote drove the class out, and the split shows you the roles while they’re still cheap to extract.
How it works
Every recorded call knows its caller’s class and its receiver’s class. For each class,
split_interface groups its public methods by which callers use them. Where the callers fall into
groups that share no methods, each group is a role the class is playing. The quick-fix extracts each
role into its own object or module and points each caller at the one it uses.
Limits
- A hint, never an error. Partial use is normal. A class used in three different ways isn’t necessarily wrong, so this never fails a build.
- Only recorded callers count. A caller that never ran isn’t in the grouping, so check the suggestion against the code before splitting.
- Overlap weakens the signal. When most callers share a core method, the groups aren’t disjoint and the report says so instead of forcing a split.
Related tools
dead_api
Find dead public API. Public methods no caller reached in any recorded run, including the ones whose only caller is their own spec. Greyed out in your editor.
See more →observed_shape
The observed contract. The messages your code actually sends to a value, read from real runs. The interface a call depends on, whether or not anyone declared it.
See more →specs_that_reach
Run only the specs that matter. The spec examples whose recorded runs reach a method, so a change reruns those and nothing else. Keeps a red-green loop in seconds.
See more →