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:

  1. Explicit composition - You can see exactly what capabilities each type has
  2. Flexibility - You can easily add or remove capabilities without changing hierarchies
  3. 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:

  1. Safety - No fragile base class problem
  2. Flexibility - Types can have multiple capabilities without deep hierarchies
  3. Clarity - Relationships are explicit: what each type contains and what it can do
  4. Composability - Build complex types from simple, focused pieces
  5. 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.