Code Quality Improvement Techniques Part 1 (opens in new tab)
The "Clone Family" anti-pattern occurs when two parallel inheritance hierarchies—such as a data model tree and a provider tree—share an implicit relationship that is not enforced by the type system. This structure often leads to type-safety issues and requires risky downcasting to access specific data types, increasing the likelihood of runtime errors during code modifications. To resolve this, developers should replace rigid inheritance with composition or utilize parametric polymorphism to explicitly link related types.
The Risks of Implicit Correspondence
Maintaining two separate inheritance trees where individual subclasses are meant to correspond to one another creates several technical hurdles.
- Downcasting Requirements: Because a base provider typically returns a base data model type, developers must manually cast the result to a specific subclass (e.g.,
as FooDataModel), which bypasses compiler safety. - Lack of Type Enforcement: The constraint that a specific provider always returns a specific model is purely implicit; the compiler cannot prevent a provider from returning the wrong model type.
- Fragile Architecture: As the system grows, ensuring that "Provider A" always maps to "Model A" becomes difficult to audit, leading to potential bugs when new developers join the project or when the hierarchy is extended.
Substituting Inheritance with Composition
When the primary goal of inheritance is simply to share common logic, such as fetching raw data, using composition or aggregation is often a superior alternative.
- Logic Extraction: Shared functionality can be moved into a standalone class, such as an
OriginalDataProvider, which is then held as a private property within specific provider classes. - Direct Type Returns: By removing the shared parent class, each provider can explicitly return its specific data model type without needing a common interface.
- Decoupling: This approach eliminates the "Clone Family" entirely by removing the need for parallel trees, resulting in cleaner and more modular code.
Leveraging Parametric Polymorphism
In scenarios where a common parent class is necessary—for example, to manage a collection of providers within a shared lifecycle—generics can be used to bridge the two hierarchies safely.
- Generic Type Parameters: By defining the parent as
ParentProvider<T>, the base class can use a type parameter for its return values rather than a generic base model. - Subclass Specification: Each implementation (e.g.,
FooProvider : ParentProvider<FooDataModel>) explicitly defines its return type, allowing the compiler to enforce the relationship. - Flexible Constraints: Developers can still utilize type bounds, such as
ParentProvider<T : CommonDataModel>, to ensure that the generics adhere to a specific interface while maintaining type safety for callers.
When designing data providers and models, avoid creating parallel structures that rely on implicit assumptions. Prioritize composition to simplify the architecture, or use generics if inheritance is required, ensuring that the relationships between classes remain explicit and verifiable by the compiler.