← BackJan 7, 2026

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.