runtimeanalyz.ing ● early access · sign-up open

← All capabilities

split_interface

CLI · LSP · MCP free

Split a fat interface. A class whose callers each use a different slice of it. It names the role interfaces to split it into, measured from who actually calls what.

Scheduler
next_after ( book Book )
first_books
Curriculum Sequencing
def next_after(book) = …
def first_books = …
Enrollment
unlocked_for ( student Student )
Unlocking
def unlocked_for(student) = …
Report
weeks_for ( book Book )
final_books
Reporting
def weeks_for(book) = …
def final_books = …
One class, three callers, and no caller uses methods from more than one group. Each group is a role 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.

dead_api

LSP · CLI free

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

CLI · LSP · MCP free

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

CLI · LSP · MCP free

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 →