Code Quality Improvement Techniques Part 24: The Value of Legacy (opens in new tab)
The LY Corporation Review Committee advocates for simplifying code by avoiding unnecessary inheritance when differences between classes are limited to static data rather than dynamic logic. By replacing complex interfaces and subclasses with simple data models and specific instances, developers can reduce architectural overhead and improve code readability. This approach ensures that configurations, such as UI themes, remain predictable and easier to maintain without the baggage of a type hierarchy.
Limitations of Inheritance-Based Configuration
- The initial implementation used a
FooScreenThemeStrategyinterface to define UI elements like background colors, text colors, and icons. - Specific themes (Light and Dark) were implemented as separate classes that overridden the interface properties.
- This pattern creates an unnecessary proliferation of types when the only difference between the themes is the specific value of the constants being returned.
- Using inheritance for simple value changes makes the code harder to follow and can lead to over-engineering.
Valid Scenarios for Inheritance
- Dynamic Logic: When behavior needs to change dynamically at runtime via dynamic dispatch.
- Sum Types: Implementing restricted class hierarchies, such as Kotlin
sealedclasses or Java's equivalent. - Decoupling: Separating interface from implementation to satisfy DI container requirements or to improve build speeds.
- Dependency Inversion: Applying architectural patterns to resolve circular dependencies or to enforce one-way dependency flows.
Transitioning to Data Models and Instantiation
- Instead of an interface, a single "final" class or data class (e.g.,
FooScreenThemeModel) should be defined to hold the required properties. - Individual themes are created as simple instances of this model rather than unique subclasses.
- In Kotlin, defining a class without the
openkeyword ensures that the properties are not dynamically altered and that no hidden, instance-specific logic is introduced. - This "instantiation over inheritance" strategy guarantees that properties remain static and the code remains concise.
To maintain a clean codebase, prioritize data-driven instantiation over class-based inheritance whenever logic remains constant. This practice reduces the complexity of the type system and makes the code more resilient to unintended side effects.