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:
- Interfaces โ pure contracts, satisfied implicitly (structural typing)
- 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
- Composition over Inheritance โ Wikipedia
- Is Go Object Oriented? โ spf13
- Go Interfaces โ Tour of Go
- Ruby Modules โ Ruby Docs
Comments