Rust's Approach to Code Reuse: Beyond Classical Inheritance
Classical inheritance, a cornerstone of object-oriented programming in languages like C++ and Java, allows for the creation of class hierarchies where derived classes inherit properties and behaviors from base classes. This model, often characterized by the `is-a` relationship, has been a familiar paradigm for many developers transitioning from older languages. However, Rust charts a different course, eschewing direct class inheritance in favor of a powerful combination of structs and traits.
While the absence of direct inheritance might initially seem like a limitation, Rust's design provides a flexible and robust alternative that addresses the core needs of code reuse and polymorphism. The language offers mechanisms that allow developers to achieve similar outcomes to inheritance, often with greater clarity and compile-time safety. This article explores how Rust handles code reuse, presenting scenarios that demonstrate its unique approach, which can be summarized as a nuanced 'yes, no, and maybe' when compared to traditional inheritance models.
The "No": Rust's Rejection of Classical Inheritance
Rust does not support classical class inheritance. You cannot define a struct `Derived` that inherits fields and methods directly from a struct `Base` in the way you would in C++ or Java. This is a deliberate design choice aimed at avoiding the complexities and potential pitfalls associated with deep inheritance hierarchies, such as the fragile base class problem and the limitations of multiple inheritance.
Instead of inheriting behavior, Rust emphasizes composition. Developers are encouraged to build complex types by embedding instances of other types within their structs. This `has-a` relationship promotes modularity and reduces tight coupling between components. For instance, a `Car` struct might contain an `Engine` struct and a `Transmission` struct, rather than inheriting from an `Automobile` base class.
The "Yes": Traits as a Form of Polymorphism and Code Reuse
Where Rust truly shines and offers an alternative to inheritance is through its powerful trait system. Traits in Rust are analogous to interfaces in other languages, defining a set of methods that a type must implement. They enable polymorphism, allowing you to write functions that operate on any type that implements a specific trait, regardless of its concrete type.
Consider a scenario with different types of displays, like e-paper displays. You might have a `DisplayController` trait with methods such as `init()`, `clear()`, and `display_image()`. Then, concrete structs like `Ssd1680Controller` and `Ssd1681Controller` would implement this `DisplayController` trait. Functions that need to interact with any compatible display controller can accept a generic type that implements `DisplayController`, achieving a form of subtype polymorphism without classical inheritance.

Furthermore, Rust allows for trait inheritance, where one trait can inherit from another. This means a new trait can extend the functionality of an existing one, requiring implementors to provide all methods from both the new and the parent trait. This mechanism provides a structured way to build up capabilities and reuse trait definitions, offering a powerful alternative to multiple inheritance in class-based systems.
The "Maybe": Struct Composition and Trait Objects
The "maybe" aspect arises when combining Rust's features. Struct composition, as mentioned earlier, allows a struct to own instances of other structs. If these embedded structs expose their functionality through methods, the outer struct can effectively delegate calls to them. This is a form of behavior reuse, though not through direct inheritance.
Moreover, Rust supports trait objects. These are dynamically dispatched types that allow you to refer to any type that implements a specific trait. For example, you could have a `Vec
The combination of structs implementing traits, trait inheritance, and trait objects means that while Rust doesn't have traditional class inheritance, it provides a rich set of tools to achieve similar goals: code reuse, polymorphism, and the ability to model complex relationships between different data types. The key difference is that Rust's approach often leads to more explicit, compile-time checked code, reducing the likelihood of runtime errors common in deeply nested inheritance hierarchies.
Practical Example: E-Paper Display Drivers
In the context of maintaining `epdsi`, a `no_std` driver crate for e-paper displays, these concepts become concrete. Different e-paper display models, such as the SSD1680 and SSD1681, share many initialization sequences and display commands but have subtle differences in timing or specific command codes. Instead of inheriting from a base `EpaperDisplay` class, each controller struct (`Ssd1680Controller`, `Ssd1681Controller`) implements a common `EpaperDisplay` trait. This trait defines the essential operations like `init()`, `send_command()`, and `send_data()`. The specific implementations within each struct handle the model-specific details.
A function that needs to update any compatible e-paper display might accept a generic parameter constrained by the `EpaperDisplay` trait: fn update_display<D: EpaperDisplay>(display: &mut D, image_data: &[u8]). This function works seamlessly with any struct that implements `EpaperDisplay`, demonstrating polymorphism. If a new display controller, say `Ssd1682Controller`, is added, it simply needs to implement the `EpaperDisplay` trait, and the existing `update_display` function will work with it without modification. This is Rust's way of achieving code reuse and polymorphism – powerful, explicit, and safe.
Conclusion: Rust's Pragmatic Approach
Rust's stance on inheritance is clear: it rejects classical class inheritance in favor of traits and composition. This decision encourages developers to think about relationships between types in terms of shared behavior (traits) and containment (composition) rather than strict `is-a` hierarchies. The result is code that is often more modular, easier to reason about, and less prone to the runtime surprises that can plague complex inheritance models. While it may require a mental shift for developers accustomed to traditional OOP, Rust's approach offers a robust and modern path to building complex software systems.
