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.
Abstract as in without to many details. Its a formal way to say "we're being vague".
Saying, "I have a form of transportation that I take to work." is more abstract than, "I have a car that I take to work". Of course somewhere something knows exactly what you're taking to work. This is about not having to know exactly what, everywhere. This idea is called abstraction.
How it's used:
An abstract or parent class in most any OOP language is a place to centralize reusable generalized methods and provide the interface to more specified methods whose code resides on more concrete or child classes.
So if I provided an abstract class called Transportation
with a takeMeToWork()
method on it you could call takeMeToWork()
on anything that inherited from Transportation and expect to end up at work. You wouldn't know if you were taking a Car
or a Bicycle
to work but you'd be going to work. Transportation
would only promise that there will be a takeMeToWork()
method. It wouldn't define how it works and in fact won't work until it is provided with a Car
or Bicycle
that does.
If you demand that every form of Transportation
have the same cup holder for your drink you could put a useCupHolder()
method in the Transportation class once and never have to write it again. It would always be there working exactly the same way. Depending on the language or version of the language that trick might not be available to an interface or "trait". Other than providing default implementation abstract classes aren't much different from traits. This question deals with those differences.
The problem with appreciating this metaphor is that it's hard to see the point until you're in a situation where it proves useful. Right now it probably sounds like a lot of fancy hard to understand stuff that will only make solving whatever problem harder. And that is actually true. Until you've found yourself working with code complicated enough to make use of this and mastered abstraction it really is only going to make things harder. Once you get it though it makes everything easier. Especially when you're not writing code alone. This next metaphor isn't a classic one but it is my favorite:
Why do we have hoods on cars?
(Or bonets for you non-americans)
The car runs fine without it. All the cool engine stuff is easier to get to without it. So what's it for? Without the hood I can sit on the engine block, jam a poll into the rack and pinon, grab the throttle, and drive the car. Now I can do really cool things like change the oil at 50 miles per hour.
We've discovered over the years that people really are more comfortable driving without a dip stick in their face. So we put the hood on the car and provided heated seats, steering wheels, and gas peddles. This makes us comfortable and prevents us from getting a pant leg caught in the fan belt.
In software we provide the same thing with abstraction. It comes in many forms, abstract classes, traits, facade patterns, etc. Even the humble method is a form of abstraction.
The more complex the problem you're solving the more you'll be better off using some wise abstractions. And, well your car looks cooler with the hood on it.
An abstract class simply provides a defined interface, a number of methods. Any subclass of the abstract class can be thought of as a specific implementation or refinement of that class.
That allows you to define a method that takes a Shape
argument, and the body of the method may then use that interface, e.g. call the shape's draw
method, irrespective of the type of shape that was given to it.
In terms of the type system, asking for a Shape
ensures statically (at compile time) that you can only pass an object that satisfies the Shape
interface, so it is guaranteed to contain the draw
method.
Personally, I prefer to use traits instead of abstract classes, the latter has bit of Java smell for me in Scala. The difference is that an abstract class may have constructor arguments. A concrete implementing class, on the other hand, is free to implement more than one trait, whereas it can only extend a single class (abstract or not).