Divergent Change
debt(d7/e5/b7/t5)
Closest to 'only careful code review or runtime testing' (d7). While detection_hints lists phpstan and phpmd, automated detection is explicitly marked 'no' — these tools can flag class complexity metrics but cannot reliably detect that a class changes for multiple unrelated reasons. Identifying divergent change requires understanding the semantic reasons behind changes over time, which demands careful code review or historical analysis of commit patterns.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix indicates splitting a class along responsibility lines, which means extracting new classes, creating interfaces, updating all call sites within the component, and adjusting dependency injection. This is a significant refactor but typically contained to one area of the codebase rather than cross-cutting.
Closest to 'strong gravitational pull' (b7). Divergent change applies across all contexts (web, cli, queue-worker) and represents an SRP violation that shapes how changes propagate through the system. Once a class becomes a multi-responsibility hub, every feature touching any of those concerns must navigate that class. The common_mistakes note that it's 'easier to split early than after it is heavily depended upon' — the burden compounds as the class accumulates dependencies.
Closest to 'notable trap' (t5). The misconception clearly states that developers confuse divergent change with shotgun surgery, thinking they are opposites when they are actually complementary smells. This is a documented gotcha that most developers eventually learn through experience with refactoring literature, but the initial confusion about the direction of the smell (one class, many reasons vs. one change, many classes) catches intermediate developers.
Also Known As
TL;DR
Explanation
Divergent change is the opposite of Shotgun Surgery. It occurs when a single class is modified every time a different kind of change is needed — you change it for database updates, then again for UI changes, then again for business rule changes. This indicates the class handles multiple distinct concerns and should be split. It is a violation of the Single Responsibility Principle. The refactoring is to extract the different concerns into separate classes.
Common Misconception
Why It Matters
Common Mistakes
- A service class that handles both business logic and persistence — database changes and rule changes both require editing it.
- Controllers that contain validation, business logic, and response formatting — three reasons to change.
- Not applying Single Responsibility Principle when a class first becomes large — easier to split early than after it is heavily depended upon.
- Treating divergent change as an acceptable trade-off — it always increases defect rates over time.
Code Examples
// One class changes for multiple unrelated reasons
class OrderProcessor {
public function processPayment(): void {} // changes with payment logic
public function sendConfirmation(): void {} // changes with email templates
public function saveToDatabase(): void {} // changes with DB schema
}
// Split by reason to change (Single Responsibility)
class PaymentProcessor { public function charge(Order \$o): void {} }
class OrderMailer { public function sendConfirmation(Order \$o): void {} }
class OrderRepository { public function save(Order \$o): void {} }
class OrderService { // thin orchestrator
public function place(Order \$o): void {
\$this->payment->charge(\$o);
\$this->repository->save(\$o);
\$this->mailer->sendConfirmation(\$o);
}
}