Skip to main content
โšก Calmops

Interface Inheritance vs Implementation Inheritance in Ruby and Go

Introduction

Two fundamental concepts in object-oriented design are interface inheritance and implementation inheritance. Choosing between them has a major impact on how flexible, testable, and maintainable your code is.

This article explores both concepts with examples in Ruby and Go โ€” two languages that take very different approaches.

What is Implementation Inheritance?

Implementation inheritance means a subclass inherits both the interface (method signatures) and the implementation (method bodies) from a parent class.

class Animal
  def breathe
    "Inhale... Exhale..."
  end

  def sleep
    "Zzz..."
  end
end

class Dog < Animal
  def speak
    "Woof!"
  end
end

dog = Dog.new
puts dog.breathe  # => "Inhale... Exhale..."  (inherited)
puts dog.speak    # => "Woof!"

Dog gets breathe and sleep for free. Convenient, but it creates tight coupling.

The Fragile Base Class Problem

When you change the parent class, all subclasses are affected:

class Animal
  def breathe
    "Using gills now..."  # changed
  end
end

puts Dog.new.breathe  # => "Using gills now..." โ€” unexpected side effect

Deep hierarchies amplify this. A change at the top ripples through every level.

What is Interface Inheritance?

Interface inheritance means a class commits to a contract (method signatures) without inheriting any implementation. Each class provides its own implementation.

In Ruby, modules serve as interfaces:

module Speakable
  def speak
    raise NotImplementedError, "#{self.class} must implement speak"
  end
end

module Swimmable
  def swim
    raise NotImplementedError, "#{self.class} must implement swim"
  end
end

class Duck
  include Speakable
  include Swimmable

  def speak; "Quack!"; end
  def swim;  "Paddling..."; end
end

class Dog
  include Speakable
  def speak; "Woof!"; end
end

puts Duck.new.speak  # => "Quack!"
puts Dog.new.speak   # => "Woof!"

No shared code โ€” only a shared contract.

Ruby: Modules as Mixins

Ruby uses modules for both patterns. As a mixin, a module shares concrete implementation:

module Timestampable
  def created_at
    @created_at ||= Time.now
  end

  def touch
    @updated_at = Time.now
  end
end

class Post
  include Timestampable
  def initialize(title); @title = title; end
end

class Comment
  include Timestampable
  def initialize(body); @body = body; end
end

puts Post.new("Hello").created_at

This gives code reuse without deep class hierarchies.

Go’s Approach: Interfaces Only

Go has no class inheritance at all. It uses:

  1. Interfaces โ€” pure contracts, satisfied implicitly (structural typing)
  2. Struct embedding โ€” composition for code reuse

Interfaces in Go

type Speaker interface {
    Speak() string
}

type RealDuck struct{}

func (d RealDuck) Speak() string { return "Quack!" }

// Any type with Speak() satisfies Speaker โ€” no explicit declaration needed
func MakeNoise(s Speaker) {
    fmt.Println(s.Speak())
}

MakeNoise(RealDuck{})  // => Quack!

Struct Embedding for Code Reuse

type Animal struct{ Name string }

func (a Animal) Breathe() string {
    return a.Name + " is breathing"
}

type Dog struct {
    Animal  // embedded โ€” Dog gets Breathe() for free
    Breed string
}

dog := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(dog.Breathe())  // => Rex is breathing

Dog has an Animal (composition), not is an Animal (inheritance).

Composition Over Inheritance

The Gang of Four principle: favor composition over inheritance. Build objects from smaller, focused pieces instead of tall hierarchies.

class Logger
  def log(msg); puts "[LOG] #{msg}"; end
end

class Notifier
  def notify(msg); puts "[NOTIFY] #{msg}"; end
end

class OrderProcessor
  def initialize(logger: Logger.new, notifier: Notifier.new)
    @logger   = logger
    @notifier = notifier
  end

  def process(order)
    @logger.log("Processing order #{order[:id]}")
    @notifier.notify("Order #{order[:id]} complete")
  end
end

OrderProcessor.new.process({ id: 42 })
# => [LOG] Processing order 42
# => [NOTIFY] Order 42 complete

OrderProcessor uses Logger and Notifier โ€” it doesn’t inherit from them. Easy to swap for testing.

When to Use Each

Approach Use When
Implementation inheritance Sharing concrete behavior across closely related classes
Interface inheritance Defining contracts multiple unrelated types must satisfy
Composition Combining behaviors; when you want flexibility and testability
Mixins (Ruby) Sharing utility methods across unrelated classes

Key Takeaways

  • Implementation inheritance is convenient but creates tight coupling
  • Interface inheritance defines contracts without coupling implementations
  • Ruby supports both via class inheritance and modules; prefer modules for shared behavior
  • Go enforces composition โ€” no class inheritance, only interfaces and struct embedding
  • Composition over inheritance leads to more flexible, testable code

Resources

Comments