Inheritance vs. Composition
One of the most important differences between Oxide and traditional object-oriented languages is how it approaches code reuse. Rather than using class inheritance, Oxide favors composition and trait implementation. This chapter explores why Oxide made this choice and how to use composition effectively.
The Problem with Inheritance
Traditional OOP languages use inheritance to achieve code reuse and polymorphism. A derived class inherits all the behavior of its parent class and can override or extend that behavior. While this seems convenient, inheritance has several well-documented problems:
The Fragile Base Class Problem
When you inherit from a class, you depend on the internals of that class. If the base class author changes the implementation in a way you don't expect, your derived class can break. For example:
#![allow(unused)] fn main() { // Traditional inheritance pseudocode class Bird { fn fly() { /* implementation */ } } class Penguin extends Bird { // Inherits fly(), which doesn't match penguin behavior! // Need to override with an error or fake behavior } }
Tight Coupling
Inheritance creates tight coupling between parent and child classes. The derived class must understand the parent's implementation details, making changes risky.
Deep Hierarchies
Inheritance encourages deep class hierarchies that are hard to navigate, maintain, and reason about:
Animal
├── Mammal
│ ├── Cat
│ ├── Dog
│ └── Whale
└── Bird
├── Eagle
├── Penguin
└── Ostrich
Adding a new category or changing the hierarchy becomes increasingly difficult.
Oxide's Solution: Composition and Traits
Oxide avoids these problems by emphasizing composition and trait-based design. Instead of "is-a" relationships (inheritance), Oxide uses "has-a" relationships (composition) and explicit behavior contracts (traits).
Composition: Building with Smaller Pieces
Rather than inheriting from a base class, build your types from smaller, focused components:
public struct Bird {
public name: String,
public age: UInt,
public canFly: Bool,
}
public struct FlyingAbility {
public maxAltitude: Int,
public speed: Float,
}
public struct Penguin {
public bird: Bird, // Has a bird, not is a bird
public swimSpeed: Float,
// Note: no flying ability - penguins can't fly
}
public struct Eagle {
public bird: Bird, // Has a bird, not is a bird
public flyingAbility: FlyingAbility,
}
This approach is more flexible because:
- Explicit composition - You can see exactly what capabilities each type has
- Flexibility - You can easily add or remove capabilities without changing hierarchies
- No false hierarchies - Penguins don't pretend to be flying birds
Traits Define Behavior Contracts
Traits specify what behaviors a type can perform. Multiple types can implement the same trait, creating a polymorphic interface without inheritance:
public trait Animal {
fn getName(): String
fn makeSound(): String
}
public trait Swimmer {
fn swim(distance: Float)
fn getSwimSpeed(): Float
}
public trait Flyer {
fn fly(altitude: Int)
fn getMaxAltitude(): Int
}
// Penguin implements Animal and Swimmer, but not Flyer
extension Penguin: Animal {
fn getName(): String {
self.bird.name
}
fn makeSound(): String {
"squawk".toString()
}
}
extension Penguin: Swimmer {
fn swim(distance: Float) {
println!("Swimming \(distance)m at \(self.swimSpeed) m/s")
}
fn getSwimSpeed(): Float {
self.swimSpeed
}
}
// Eagle implements all three
extension Eagle: Animal {
fn getName(): String {
self.bird.name
}
fn makeSound(): String {
"screech".toString()
}
}
extension Eagle: Flyer {
fn fly(altitude: Int) {
println!("Flying to \(altitude)m")
}
fn getMaxAltitude(): Int {
self.flyingAbility.maxAltitude
}
}
extension Eagle: Swimmer {
fn swim(distance: Float) {
println!("Swimming \(distance)m")
}
fn getSwimSpeed(): Float {
20.5 // Eagles swim slower than penguins
}
}
Practical Example: UI Components
Here's a realistic example showing composition and traits in action:
public struct Button {
public label: String,
public enabled: Bool,
}
public struct Clickable {
public onClickHandler: () -> Unit,
}
public struct Styleable {
public backgroundColor: String,
public textColor: String,
public borderWidth: Int,
}
// A button is composed of these behaviors
public struct StyledButton {
public button: Button,
public clickable: Clickable,
public styleable: Styleable,
}
public trait Interactive {
fn handleClick()
}
public trait Visual {
fn render(): String
}
extension StyledButton: Interactive {
fn handleClick() {
if self.button.enabled {
self.clickable.onClickHandler()
}
}
}
extension StyledButton: Visual {
fn render(): String {
"<button style='background: \(self.styleable.backgroundColor); color: \(self.styleable.textColor);'>\(self.button.label)</button>".toString()
}
}
// Usage
fn createButton(): StyledButton {
StyledButton {
button: Button {
label: "Click me".toString(),
enabled: true,
},
clickable: Clickable {
onClickHandler: {
println!("Button clicked!")
},
},
styleable: Styleable {
backgroundColor: "#007bff".toString(),
textColor: "white".toString(),
borderWidth: 1,
},
}
}
Default Trait Implementations
Traits can provide default implementations for common behavior, reducing duplication:
public trait Drawable {
fn draw(): String {
"[Drawing object]".toString()
}
fn getSize(): (Int, Int) {
(100, 100)
}
}
public struct Circle {
public radius: Int,
}
// Use default implementations
extension Circle: Drawable {
// Both draw() and getSize() use defaults
}
// Or override them
extension Circle: Drawable {
fn draw(): String {
"○ (radius: \(self.radius))".toString()
}
fn getSize(): (Int, Int) {
(self.radius * 2, self.radius * 2)
}
}
Delegation Pattern
Sometimes you want one type to forward method calls to another. This is called delegation and is a key composition pattern:
public struct Logger {
private logLevel: String,
fn log(message: String) {
println!("[\(self.logLevel)] \(message)")
}
}
public struct Application {
public logger: Logger,
public name: String,
}
extension Application {
fn log(message: String) {
// Delegate to the logger
self.logger.log(message)
}
}
// Usage
var app = Application {
logger: Logger { logLevel: "INFO".toString() },
name: "MyApp".toString(),
}
app.log("Application started") // Delegates to app.logger.log()
When to Use Composition vs. Traits
Use composition (has-a) when:
- One type contains instances of other types
- You want to reuse implementation
- The relationship is "part of" or "contains"
Use traits (can do) when:
- Multiple types share the same behavior interface
- You want polymorphism (same operation, different implementations)
- You want to define a contract that types must satisfy
Often use both together:
- Compose types from smaller components
- Implement traits to define how they behave
- Use traits to write generic code that works with any type implementing the trait
Code Reuse Through Traits
Here's how to achieve the code reuse benefit of inheritance using traits:
public trait Named {
fn getName(): String
}
public trait Identifiable {
fn getId(): String
}
public struct Person {
public id: String,
public name: String,
public age: UInt,
}
public struct Company {
public id: String,
public name: String,
public employeeCount: UInt,
}
// Both types can implement the same traits
extension Person: Named {
fn getName(): String {
self.name
}
}
extension Person: Identifiable {
fn getId(): String {
self.id
}
}
extension Company: Named {
fn getName(): String {
self.name
}
}
extension Company: Identifiable {
fn getId(): String {
self.id
}
}
// Write generic code that works with any Named type
fn greet<T: Named>(entity: &T) {
println!("Hello, \(entity.getName())!")
}
fn main() {
let person = Person {
id: "p123".toString(),
name: "Alice".toString(),
age: 30,
}
let company = Company {
id: "c456".toString(),
name: "TechCorp".toString(),
employeeCount: 100,
}
greet(&person) // Works!
greet(&company) // Works too!
}
Comparison with Rust
Rust doesn't have classes or inheritance either. Both Rust and Oxide use the same composition + traits approach:
Rust:
#![allow(unused)] fn main() { impl Animal for Penguin { fn make_sound(&self) -> String { "squawk".to_string() } } }
Oxide:
extension Penguin: Animal {
fn makeSound(): String {
"squawk".toString()
}
}
The underlying semantics are identical; Oxide just uses different syntax that emphasizes extending a type with additional capabilities.
Summary
Oxide avoids inheritance in favor of composition and traits because:
- Safety - No fragile base class problem
- Flexibility - Types can have multiple capabilities without deep hierarchies
- Clarity - Relationships are explicit: what each type contains and what it can do
- Composability - Build complex types from simple, focused pieces
- Explicitness - The compiler forces you to be clear about what you're doing
Key takeaways:
- Use composition to structure types (has-a relationships)
- Use traits to define behavior contracts (can-do capabilities)
- Implement the same trait on different types for polymorphism
- Write generic code using trait bounds to accept any type implementing a trait
In the next section, we'll explore trait objects, which enable runtime polymorphism when you need to work with multiple types through a common interface.