Code Quality Improvement Techniques Part 14 (opens in new tab)
Applying the Single Responsibility Principle is a fundamental practice for maintaining high code quality, but over-fragmenting logic can inadvertently lead to architectural complexity. While splitting classes aims to increase cohesion, it can also scatter business constraints and force callers to manage an overwhelming number of dependencies. This post explores the "responsibility of assigning responsibility," arguing that sometimes maintaining a slightly larger, consolidated class is preferable to creating fragmented "Ravioli code."
Initial Implementation and the Refactoring Drive
The scenario involves a dynamic "Launch Button" that can fire rockets, fireworks, or products depending on its mode.
- The initial design used a single
LaunchButtonBinderthat held references to all possibleLaunchertypes and an internal enum to select the active one. - To strictly follow the Single Responsibility Principle, developers often attempt to split this into two parts: a binder for the button logic and a selector for choosing the mode.
- The refactored approach utilized a
LaunchBinderSelectorto manage multipleLaunchButtonBinderinstances, using anisEnabledflag to toggle which logic was active.
The Problem of Scattered Constraints and State
While the refactored classes are individually simpler, the overall system becomes harder to reason about due to fragmented logic.
- Verification Difficulty: In the original code, the constraint that "only one thing launches at a time" was obvious in a single file; in the refactored version, a developer must trace multiple classes and loops to verify this behavior.
- State Redundancy: Adding an
isEnabledproperty to binders creates a risk of state synchronization issues between the selector’s current mode and the binders' internal flags. - Information Hiding Trade-offs: Attempting to hide implementation details often forces the caller to resolve all dependencies (binders, buttons, and launchers) manually, which can turn the caller into a bloated "God class."
Avoiding "Ravioli Code" Through Balanced Design
The pursuit of granular responsibilities can lead to "Ravioli code," where the system consists of many small, independent components but lacks a clear, cohesive structure.
- The original implementation’s advantage was that it encapsulated all logic related to the launch button's constraints in one place.
- When deciding to split a class, developers must evaluate if the move improves the overall system or simply shifts the burden of complexity to the caller.
- Effective design requires balancing individual class cohesion with the overhead of inter-module coupling and dependency management.
When refactoring for code quality, prioritize the clarity of the overall system over the dogmatic pursuit of small classes. If splitting a class makes it harder to verify business constraints or complicates the caller's logic significantly, it may be better to keep those related responsibilities together.