inheritance

3 posts

line

Code Quality Improvement Techniques Part (opens in new tab)

LY Corporation’s technical review highlights that making a class open for inheritance imposes a "tax" on its internal constraints, particularly immutability. While developers often use inheritance to create specialized versions of a class, doing so with immutable types can allow subclasses to inadvertently or intentionally break the parent class's guarantees. To ensure strict data integrity, the post concludes that classes intended to be immutable should be made final or designed around read-only interfaces rather than open for extension. ### The Risks of Open Immutable Classes * Kotlin developers often wrap `IntArray` in an `ImmutableIntList` to avoid the overhead of boxed types while ensuring the collection remains unchangeable. * If `ImmutableIntList` is marked as `open`, a developer might create a `MutableIntList` subclass that adds a `set` method to modify the internal `protected valueArray`, violating the "Immutable" contract of the parent type. * Even if the internal state is `private`, a subclass can override the `get` method to return dynamic or state-dependent values, effectively breaking the expectation that the data remains constant. * These issues demonstrate that any class with a "fundamental" name should be carefully guarded against unexpected inheritance in different modules or packages. ### Establishing Safe Inheritance Hierarchies * Mutable objects should not inherit from immutable objects, as this inherently violates the immutability constraints established by the parent. * Conversely, immutable objects should not inherit from mutable ones; this often leads to runtime errors (such as `UnsupportedOperationException`) when a user attempts to call modification methods like `add` or `set` on an immutable instance. * The most effective design pattern is to use a "read-only" (unmodifiable) interface as a common parent, similar to how Kotlin distinguishes between `List` and `MutableList`. * In this structure, mutable classes can inherit from the read-only parent without issue (adding new methods), and immutable classes can inherit from the read-only parent while adding stricter internal constraints. To maintain high code quality and prevent logic errors, developers should default to making classes final when immutability is a core requirement. If shared functionality is needed across different types of lists, utilize composition or a shared read-only interface to ensure that the "immutable" label remains a truthful guarantee.

line

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 `FooScreenThemeStrategy` interface 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 `sealed` classes 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 `open` keyword 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.

line

Code Quality Improvement Techniques Part 19: Child Lock (opens in new tab)

The "child lock" technique focuses on improving code robustness by restricting the scope of what child classes can override in an inheritance hierarchy. By moving away from broad, overridable functions that rely on manual `super` calls, developers can prevent common implementation errors and ensure that core logic remains intact across all subclasses. This approach shifts the responsibility of maintaining the execution flow to the parent class, making the codebase more predictable and easier to maintain. ## Problems with Open Functions and Manual Super Calls Providing an `open` function in a parent class that requires child classes to call `super` creates several risks: * **Missing `super` calls:** If a developer forgets to call `super.bind()`, the essential logic in the parent class (such as updating headers or footers) is skipped, often leading to silent bugs that are difficult to track. * **Implicit requirements:** Relying on inline comments to tell developers they must override a function is brittle. If the method isn't `abstract`, the compiler cannot enforce that the child class implements necessary logic. * **Mismatched responsibilities:** When a single function handles both shared logic and specific implementations, the responsibility of the code becomes blurred, making it easier for child classes to introduce side effects or incorrect behavior. ## Implementing the "Child Lock" with Template Methods To resolve these issues, the post recommends a pattern often referred to as the Template Method pattern: * **Seal the execution flow:** Remove the `open` modifier from the primary entry point (e.g., the `bind` method). This prevents child classes from changing the overall sequence of operations. * **Separate concerns:** Move the customizable portion of the logic into a new `protected abstract` function. * **Enforced implementation:** Because the new function is `abstract`, the compiler forces every child class to provide an implementation, ensuring that specific logic is never accidentally omitted. * **Guaranteed execution:** The parent class calls the abstract method from within its non-overridable method, ensuring that shared logic (like UI updates) always runs regardless of how the child is implemented. ## Refining Overridability and Language Considerations Designing for inheritance requires careful control over how child classes interact with parent logic: * **Avoid "super" dependency:** Generally, if a child class must explicitly call a parent function to work correctly, the inheritance structure is too loose. Exceptions are usually limited to lifecycle methods like `onCreate` in Android or constructors/destructors. * **C++ Private Virtuals:** In C++, developers can use `private virtual` functions. These allow a parent class to define a rigid flow in a public method while still allowing subclasses to provide specific implementations for the private virtual components, even though the child cannot call those functions directly. To ensure long-term code quality, the range of overridability should be limited as much as possible. By narrowing the interface between parent and child classes, you create a more rigid "contract" that prevents accidental bugs and clarifies the intent of the code.