Inheritance, Polymorphism, and the Rust Alternative
Rust deliberately eschews classical inheritance in favor of composition, traits, and generic programming. This article examines why inheritance is often misâused in OOP, how it conflates data and behaviour, and presents idiomatic Rust patternsâsuch as enums, trait objects, and policy-based designâto replace traditional hierarchies.
Rustâs design deliberately rejects classical objectâoriented inheritance in favor of a clearer separation between data, behaviour, and dynamic dispatch.
In the OOP world, inheritance traditionally provides three intertwined features: data inheritance (the "hasâa" relationship hidden behind an "isâa" syntax), method inheritance, and polymorphic dispatch via virtual tables. Rustâs type system and module system split these concepts into distinct units: structs (pure data), traits (pure behaviour), and modules (namespaces and visibility). By keeping these concepts separate, Rust avoids the pitfalls that arise when a typeâs interface is unintentionally tied to its state.
Why is inheritance so alluring in the first place? Educational frameworks and a natural human tendency to model the world with hierarchical categories (âX is a Yâ) lead programmers to employ inheritance as a shorthand for such relationships. For example, a `Square` that derives from `Rectangle` feels intuitively correct because a square is a specialized rectangle. However, this intuition often ignores the fact that most realâworld applications rarely require the static, compileâtime inheritance that classic OOP promises.
### What inheritance really does
From the perspective of a record type, inheriting from a parent simply creates an anonymous field of the parent type. A `Circle` that extends `Shape` is equivalent to a struct containing a `Shape` field whose name is omitted by the language. Therefore, the âisâaâ relationship can be reduced to a straightforward âhasâa.â Rust explicitly encourages this pattern through field composition and explicit delegation, making implicit named fields unnecessary.
When methods are involved, the situation becomes more complex. A class with virtual methods introduces a hidden interface; any type that inherits from such a class must implement the interface while also owning the parentâs data. This tight coupling between interface and state makes it impossible to substitute a proxy or an alternative storage medium without refactoring the entire hierarchy.
### The confluence of record, module, and interface
A C++ or Java class conflates three distinct concerns:
1. **Record** â the data layout of the object.
2. **Module** â encapsulation of methods and visibility control.
3. **Interface** â the set of virtual methods that enable dynamic dispatch.
In Rust, each of these concerns occupies a separate namespace: structs for data, traits for behaviour, and modules for namespacing. This separation eliminates an unwanted dependency: a type can provide behaviour through a trait without owning any particular set of fields, and it can hold state through composition without being forced to expose a virtual table.
### Why inheritance often backfires
When a subclass overrides a method, it typically has access to the inherited data it cannot separate from the interface contract. A programmer may inadvertently rely on a particular state layout, making future evolution of the component brittle. Moreover, proxy patterns become difficult to implement because the required state cannot be moved outside of the inherited object.
### Rustâstyle alternatives
#### 1. Composition with explicit fields
Instead of inheriting a parent, a child type owns it as a named field. Delegated method calls make the relationship explicit:
```rust
struct Circle {
shape: Shape,
center: Point,
radius: u32,
}
impl Circle {
fn color(&self) -> Color {
self.shape.color
}
}
```
This pattern removes the illusion of hidden inheritance and grants the developer full control over field names.
#### 2. Enums to model closed hierarchies
When a type must represent one of a limited set of variants, an enum gives a zeroâcost, typeâsafe union.
```rust
enum Message {
Ping(PingMessage),
Pong(PongMessage),
Request(RequestMessage),
Response(ResponseMessage),
}
```
`Message` can be matched exhaustively, guaranteeing that all variants are handled and that new variants cannot appear without intent.
#### 3. Trait objects and static dispatch
Traits replace the polymorphic interface of a base class. For dynamic dispatch, a trait object (`Box`) provides the classic virtual table mechanism without needing a public inheriting field.
```rust
trait Shape {
fn draw(&self, surface: &mut Surface);
}
struct Circle { /* ... */ }
impl Shape for Circle { /* ... */ }
```
For compileâtime dispatch, generic parameters can substitute trait bounds:
```rust
struct SocketHandler {
buffer: CircularBuffer,
protocol: P,
}
```
Here, `SocketHandler` is a policyâbased container; each protocol implements the required behaviour, and the handler composes that behaviour.
#### 4. Policyâbased design (generic strategies)
Many class hierarchies that differ only in algorithmic strategy can be expressed through type parameters. This pattern, often called traits or policies in the Gang of Four terminology, keeps the core data and algorithm separate.
```rust
trait SocketProtocol {
fn message_size(&self, data: &[u8]) -> usize;
fn process_message(&mut self, data: &[u8]) -> Result<()>;
}
struct SocketHandler {
buffer: CircularBuffer,
protocol: P,
}
```
`SocketHandler` owns a `SocketProtocol` implementation, enabling different behaviours without subclassing.
### Choosing the right pattern
Not every OOP hierarchy needs to be refactored with Rustâs idioms. The key is to identify whether the inheritance relationship is a *pure data* relationship, a *pure behaviour* relationship, or an intertwined mixture. When the mixture is unavoidable, split it: put the data in a struct and the behaviour in a trait. When the hierarchy is simply a different variant of an operation, use an enum. When performance and compileâtime guarantees are paramount, use generics and trait bounds.
### Conclusion
Rustâs reluctance to adopt classical inheritance is not a limitation but an intentional design choice that increases clarity and safety. By treating data, behaviour, and namespacing as independent concerns, Rust empowers developers to choose the most appropriate patternâcomposition, enums, or policiesârather than defaulting to an inherited blueprint that may conceal subtle bugs. This approach aligns closely with modern softwareâengineering practices of explicitness and compositionality, and invites a clearer, more maintainable codebase.