Object-Oriented Programming Features
Is Rust OO? Encapsulation yes, inheritance no — and trait objects for runtime polymorphism.
Part of Rust Interview Mastery · Based on The Rust Programming Language
Encapsulation & Inheritance (or not)
Is Rust object-oriented? Give the precise answer.
By the classic checklist: objects carry data + behavior — yes (impl blocks). Encapsulation — yes, and stronger than most: module-level privacy by default, pub as opt-in. Inheritance — no. Rust has no class hierarchy, no extends.
So Rust is OO minus inheritance. Polymorphism comes from generics (static) and trait objects (dynamic); code reuse from composition and default trait methods. The book's thesis: the useful OO ideas survive; the dangerous one (implementation inheritance) got replaced.
Show encapsulation in Rust — the AveragedCollection pattern.
pub struct AveragedCollection {
list: Vec<i32>, // private: can't be poked from outside
average: f64,
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn average(&self) -> f64 { self.average }
fn update_average(&mut self) { /* private helper */ }
}
Field privacy is the mechanism — list can't be touched externally, so the average invariant can't be violated. Methods are the only door. Note Rust makes this structural: privacy at module level, not just social convention like Python's _name.
Why no inheritance, and what does Rust offer instead?
Inheritance does two things — code reuse (inherited methods) and polymorphism (substituting subtypes). Rust covers both without the footguns:
- Reuse → default trait methods + composition (struct holding the reused part)
- Polymorphism → generics + trait bounds (static) or trait objects (dynamic)
What's avoided: fragile base-class problem, deep hierarchies, protected ambiguity, surprise overridden behavior. The GOF-favored "composition over inheritance" is enforced, not just advised. Trade-off is real — GUI/toolkit-style designs fit worse — but for backend Rust it's a non-issue.
Trait Objects & the State Pattern
What problem do trait objects solve — the gui library example?
Heterogeneous collections. Vec<Box<dyn Draw>> holds buttons, textfields, images — different concrete types sharing one trait, stored together and called polymorphically:
trait Draw { fn draw(&self); }
struct Screen { components: Vec<Box<dyn Draw>> }
for c in &screen.components { c.draw(); } // dynamic dispatch
Generics (Vec<T: Draw>) can't do this — T is one concrete type. Box<dyn Draw> = fat pointer (data ptr + vtable ptr), dispatch resolved at runtime. Cost: a vtable indirection per call, no inlining. Worth it for plugin-like extensibility.
What is object safety — which traits can be dyn'd?
A trait is object-safe when all its methods work behind a vtable. Violations: methods returning Self (vtable can't name the concrete type) and methods with generic params (fn f<T>(&self) — one vtable slot can't cover all T instantiations).
That's why Clone isn't object-safe (clone() -> Self) — and why Box<dyn Clone> fails. Workarounds: dyn-clone crates, clone_box methods. In interviews: name why — a vtable needs fully concrete method signatures.
Explain the blog-post state pattern and its trade-off vs enums.
Classic OO version: Post holds Box<dyn State> (Draft, PendingReview, Published as state objects); post.request_review() delegates to state.request_review(self), which returns the next state via Box<dyn State>. Behavior lives in the state objects; the type system (trait objects) drives transitions.
The Rust-flavored alternative the book then shows: separate types per state — DraftPost::request_review() -> PendingReviewPost, PendingReviewPost::publish() -> Post. Transitions become type changes: impossible states unrepresentable, zero runtime dispatch. Enum + match is the middle ground when you want runtime flexibility with exhaustive checking. Three answers, three trade-off profiles.
When do you reach for trait objects vs enums for polymorphism in practice?
- Trait object (Box<dyn T>): the set of types is open/unbounded — plugins, middleware, event handlers, test doubles, heterogeneous registries.
- Enum + match: the set is closed and known — protocol messages, AST nodes, app state machines. You get exhaustive checking, no boxing, better cache behavior, and match forces handling of new variants at every site.
Rust codebase convention leans enum-first: closed domain sets are the norm; Box<dyn> is reserved for genuinely extensible seams.