Skip to content

Inheritance

Onion supports both class inheritance and interface implementation, allowing you to build hierarchical type systems.

Extending Classes

Basic Inheritance

Use : to extend a parent class:

class Animal {
protected:
  val name: String

  public:
    def this(n: String) {
      this.name = n
    }

    def speak: String = "Some sound"
}

class Dog : Animal {
  public:
    def this(n: String): (n) { }

    def speak: String = "Woof!"
}

val dog: Dog = new Dog("Buddy")
println(dog.speak())  // "Woof!"

Calling Parent Constructor

Call the parent constructor via the super-initializer list on def this:

class Vehicle {
  val brand: String

  public:
    def this(b: String) {
      this.brand = b
    }
}

class Car : Vehicle {
  val model: String

  public:
    def this(b: String, m: String): (b) {
      this.model = m
    }
}

Method Overriding

Child classes can override parent methods:

class Shape {
  public:
    def area: Double = 0.0

    def describe: String = "A shape"
}

class Circle : Shape {
  val radius: Double

  public:
    def this(r: Double) {
      this.radius = r
    }

    def area: Double = 3.14159 * this.radius * this.radius  // Override

    def describe: String = "A circle with radius " + this.radius  // Override
}

Implementing Interfaces

Single Interface

Use <: to implement an interface:

import { java.lang.Comparable; }

class Person <: Comparable[Object] {
  val name: String
  val age: Int

  public:
    def this(n: String, a: Int) {
      this.name = n
      this.age = a
    }

    def compareTo(other: Object): Int {
      val otherPerson: Person = (other as Person)
      return this.age - otherPerson.age
    }
}

Default Methods

An interface method with a body compiles to a JVM default method: implementing classes inherit it, may override it, and dispatch stays virtual through the interface type. Expression bodies work too.

interface Greeter {
  def name(): String
  def greet(): String { return "Hello, " + this.name() }
  def shout(): String = "HEY " + this.name()
}

class K <: Greeter {
public:
  def this {}
  def name(): String { return "kota" }
}

println(new K().greet())   // Hello, kota — no override needed

Multiple Interfaces

Implement multiple interfaces with a comma-separated list:

import {
  java.io.Serializable;
  java.lang.Comparable;
}

class Student <: Serializable, Comparable[Object] {
  val id: Int
  val name: String

  public:
    def this(i: Int, n: String) {
      this.id = i
      this.name = n
    }

    def compareTo(other: Object): Int {
      val otherStudent: Student = (other as Student)
      return this.id - otherStudent.id
    }
}

Combining Inheritance and Interfaces

Use both : (extend class) and <: (implement interface):

import {
  javax.swing.JFrame;
  java.awt.event.ActionListener;
  java.awt.event.ActionEvent;
}

class Calculator : JFrame <: ActionListener {
  var result: Long

  public:
    def this {
      // JFrame constructor
      this.result = 0L
    }

    def actionPerformed(event :ActionEvent) {
      // Implement ActionListener interface
      println("Button clicked")
    }
}

Syntax:

class Child : ParentClass <: Interface1, Interface2 {
  // ...
}

Delegation Pattern

Forward Directive

Use forward to delegate interface methods to a member:

interface Logger {
  def log(message: String): void
  def count(): Int
}

class BasicLogger <: Logger {
  var n: Int

  public:
    def this { this.n = 0 }
    def log(message: String): void {
      this.n = this.n + 1
      println(message)
    }
    def count(): Int = n
}

class PrefixLogger <: Logger {
  forward val delegate: Logger

  public:
    def this(delegate: Logger) {
      this.delegate = delegate
    }

    // Custom method
    def logAll(items: String[]): void {
      foreach item: String in items {
        this.delegate.log("[app] " + item)
      }
    }
}

val logger: PrefixLogger = new PrefixLogger(new BasicLogger())
logger.logAll(new String[]{"First", "Second"})
println(logger.count())   // count() is forwarded to BasicLogger -> 2

The forward directive automatically implements the interface methods (log/count) by forwarding calls to the delegate member, so PrefixLogger only needs to add its own behaviour.

Forwarding a Generic Interface

forward also works over a parameterized generic interface, not just non-generic ones. Delegate to a List[String] member and the class satisfies List[String] for free:

import {
  java.util.List;
  java.util.ArrayList;
}

class MyList <: List[String] {
  forward val backing: List[String]

  public:
    def this(xs: List[String]) { backing = xs }
}

def main(args: String[]): void {
  val base: List[String] = new ArrayList[String]()
  base.add("a")
  base.add("b")

  // MyList is usable anywhere a List[String] is expected
  val list: List[String] = new MyList(base)
  list.add("c")
  println("size=" + list.size())   // size=3
  foreach s: String in list {
    println(s)                      // a, b, c
  }
}

The same works for a user-defined generic interface — forward to a Container[Int] member and the class implements Container[Int]:

interface Container[T] {
  def first(): T
  def count(): Int
}

class IntBox <: Container[Int] {
  public:
    def this {}
    def first(): Int { return 42 }
    def count(): Int { return 3 }
}

class Wrapper <: Container[Int] {
  forward val inner: Container[Int]

  public:
    def this(c: Container[Int]) { inner = c }
}

def main(args: String[]): void {
  val w: Container[Int] = new Wrapper(new IntBox())
  println("count=" + w.count())   // count=3
  println("first=" + w.first())   // first=42
}

Polymorphism

Type Substitution

Child objects can be assigned to parent variables:

class Animal {
  public:
    def speak: String = "Generic sound"
}

class Dog : Animal {
  public:
    def speak: String = "Woof!"
}

class Cat : Animal {
  public:
    def speak: String = "Meow!"
}

val animals: Animal[] = new Animal[3]
animals[0] = new Dog
animals[1] = new Cat
animals[2] = new Animal

foreach animal :Animal in animals {
  println(animal.speak())
}
// Output:
// Woof!
// Meow!
// Generic sound

Interface Polymorphism

import {
  java.util.List;
  java.util.ArrayList;
  java.util.LinkedList;
}

val list1: List[String] = new ArrayList[String]()  // ArrayList implements List
val list2: List[String] = new LinkedList[String]()  // LinkedList implements List

val lists: java.util.List[List[String]] = [list1, list2]
foreach list: List[String] in lists {
  list.add("Item")
  println("Size: " + list.size())
}

Abstract Concepts

Note on Abstract Methods

The compiler enforces abstract method implementation at compile time. Concrete subclasses must implement abstract members or be declared abstract:

abstract class Shape {
  public:
    abstract def area(): Double;
}

class Circle : Shape {
  val radius: Double

  public:
    def this(r: Double) {
      this.radius = r
    }

    def area(): Double = 3.14159 * this.radius * this.radius
}

Inheritance Best Practices

Favor Composition Over Inheritance

Use delegation when possible:

interface Logger {
  def log(message: String): void
  def count(): Int
}

// Instead of inheriting from a concrete BasicLogger...
// class PrefixLogger : BasicLogger { ... }

// ...prefer delegating to the Logger interface.
class PrefixLogger <: Logger {
  forward val delegate: Logger

  public:
    def this(delegate: Logger) {
      this.delegate = delegate
    }
}

Keep Hierarchies Shallow

Avoid deep inheritance chains:

// Good: Shallow hierarchy
Animal
  ├─ Dog
  └─ Cat

// Bad: Deep hierarchy
Vehicle
  └─ MotorVehicle
      └─ Car
          └─ Sedan
              └─ LuxurySedan

Override Consistently

When overriding methods, maintain the parent's contract:

class Parent {
  public:
    def process(value :Int) :Int {
      if value < 0 {
        return 0
      }
      return value * 2
    }
}

class Child : Parent {
  public:
    def process(value :Int) :Int {
      // Maintain parent's behavior
      if value < 0 {
        return 0
      }
      return value * 3  // Different implementation, same contract
    }
}

Next Steps