This particular example is not the best since Shape
should probably be a trait
, not an abstract class
.
Inheritance does two separate but related things: it lets different values implement a common interface, and it lets different classes share implementation code.
Common Interface
Suppose we've got a drawing program that needs to do things with a bunch of different shapes - Square
, Circle
, EquilateralTriangle
and so on. In the bad old days, we might do this with a bunch of if/else
statements, something like:
def drawShapes(shapes: List[Shape]) =
for { shape <- shapes } {
if(isCircle(shape))
drawDot(shape.asInstanceOf[Circle].center)
...
else if(isSquare(shape))
drawStraghtLine(shape.asInstanceOf[Square].topLeft, shape.asInstanceOf[Square].topRight)
...
}
def calculateEmptySpace(shapes: List[Shape]) =
val shapeAreas = for { shape <- shapes } yield {
if(isCircle(shape)) (shape.asInstanceOf[Circle].radius ** 2) * Math.PI
else if(isSquare(shape)) ...
}
(in Scala we'd actually use a pattern match, but let's not worry about that for the moment)
This is a kind of repetitive pattern; it would be nice to isolate the repetitive "figure out the correct type of shape, then call the right method" logic. We could write this idea (a virtual function table) ourselves:
case class ShapeFunctions[T](draw: T => Unit, area: T => Double)
object ShapeFunctions {
val circleFunctions = new ShapeFunctions[Circle]({c: Circle => ...}, {c: Circle => ...})
val squareFunctions = new ShapeFunctions[Square](...)
def forShape(shape: Any) = if(isCircle(shape)) circleFunctions
else if(isSquare(shape)) squareFunctions
else ...
}
def drawShapes(shapes: List[Shape]) =
for {shape <- shapes}
ShapeFunctions.forShape(shape).draw(shape)
But this is actually so common an idea that it's built into the language. When we write something like
trait Shape {
def draw(): Unit
def area(): Double
}
class Circle extends Shape {
val center: (Double, Double)
val radius: Double
def draw() = {...}
def area() = {...}
}
"under the hood" this is doing something very similar; it's creating a special value Circle.class
which contains this draw()
and area()
method. When you create an instance of Circle
by val circle = new Circle()
, as well as the ordinary fields center
and radius
, this Circle
has a magic, hidden field circle.__type = Circle.class
.
When you call shape.draw()
, this is sort of equivalent to shape.__type.draw(shape)
(not real syntax). Which is great, because it means that if shape
is a Square
, then the call will be Square.class.draw(shape)
(again, not real syntax), but if it's a Circle
then the call will be Circle.class.draw(shape)
. Notice how a class always gets called with a value of the correct type (it's impossible to call Square.class.draw(circle)
, because circle.draw()
always goes to the correct implementation).
Now, lots of languages have something a bit like this without the trait
part. For example, in Python, I can do:
class Square:
def draw(self): ...
class Circle:
def draw(self): ...
and when I call shape.draw()
, it will call the right thing. But if I have some other class:
class Thursday: ...
then I can call new Thursday().draw()
, and I'll get an error at runtime. Scala is a type-safe language (more or less): this method works fine:
def doSomething(s: Square): s.draw()
while this method won't compile:
def doSomething(t: Thursday): t.draw()
Scala's type system is very powerful and you can use it to prove all sorts of things about your code, but at a minimum, one of the nice things it guarantees is "you will never call a method that doesn't exist". But that presents a bit of a problem when we want to call our draw()
method on an unknown type of shape. In some languages (e.g. I believe Ceylon) you can actually write a method like this (invalid Scala syntax):
def drawAll(shapes: List[Circle or Square or EquilateralTriangle]) = ...
But even that's not really what we want: if someone writes their own Star
class, we'd like to be able to include that in the list we pass to drawAll
, as long as it has a draw()
method.
So that's where the trait
comes in.
trait Shape {
def draw(): Unit
def area(): Double
}
class Circle extends Shape {...}
means roughly "I promise that Circle
has a def draw(): Unit
method. (Recall that this really means "I promise Circle.class
contains a value draw: Circle => Unit
). The compiler will enforce your promise, refusing to compile Circle
if it doesn't implement the given methods. Then we can do:
def drawAll(shapes: List[Shape]) = ...
and the compiler requires that every shape
in shapes
is from a type with a def draw(): Unit
method. So shape.__type.draw(shape)
is "safe", and our method is guaranteed to only call methods that actually exist.
(In fact Scala also has a more powerful way of achieving the same effect, the typeclass pattern, but let's not worry about that for now.)
Sharing implementation
This is simpler, but also "messier" - it's a purely practical thing.
Suppose we have some common code that goes with an object's state. For example, we might have a bunch of different animals that can eat things:
class Horse {
private var stomachContent: Double = ...
def eat(food: Food) = {
//calorie calculation
stomachContent += calories
}
}
class Dog {
def eat(food: Food) = ...
}
Rather than writing the same code twice, we can put it in a trait
:
trait HasStomach {
var stomachContent: Double
def eat(food: Food) = ...
}
class Horse extends HasStomach
class Dog extends HasStomach
Notice that this is the same thing we wrote in the previous case, and so we can also use it the same way:
def feed(allAnimals: List[HasStomach]) = for {animal <- allAnimals} ...
But hopefully you can see that our intent is different; we might do the same thing even if eat
was an "internal" method that couldn't be called by any outside functions.
Some people have criticised "traditional" OO inheritance because it "mixes up" these two meanings. There's no way to say "I just want to share this code, I don't want to let other functions call it". These people tend to argue that sharing code should happen through composition: rather than saying that our Horse
extends HasStomach
, we should compose a Stomach
into our Horse
:
class Stomach {
val content: Double = ...
def eat(food: Food) = ...
}
class Horse {
val stomach: Stomach
def eat(food: Food) = stomach.eat(food)
}
There is some truth to this view, but in practice (in my experience) it tends to result in longer code than the "traditional OO" approach, particularly when you want to make two different types for a large, complex object with some small, minor difference between the two types.
Abstract Classes versus Traits
So far everything I've said applies equally to trait
s and abstract class
es (and to a certain extent also to class
es, but let's not go into that).
For many cases, both a trait
and an abstract class
will work, and some people advise using the difference to declare intent: if you want to implement a common interface, use a trait
, and if you want to share implementation code, use an abstract class
. But in my opinion the most important difference is about constructors and multiple inheritance.
Scala allows multiple inheritance; a class may extend
several parents:
class Horse extends HasStomach, HasLegs, ...
This is useful for obvious reasons, but can have problems in diamond inheritance cases, particularly when you have methods that call a superclass method. See Python's Super Considered Harmful for some of the problems that arise in Python, and note that in practice, most of the problems happen with constructors, because these are the methods that usually want to call a superclass method.
Scala has an elegant solution for this: abstract class
es may have constructors, but trait
s may not. A class may inherit from any number of trait
s, but an abstract class
must be the first parent. This means that any class has exactly one parent with a constructor, so it's always obvious which method is the "superclass constructor".
So in practical code, my advice is to always use trait
s where possible, and only use abstract class
for something that needs to have a constructor.