This question already has an answer here:
- Abstract class in Java 14 answers
I'm trying to understand this code example from O'Reilly's Programming Scala. I'm a JavaScript programmer and most of the explanations in the book assume a Java background. I'm looking for a simple, high-level explanation of abstract classes and what they're used for.
package shapes {
class Point(val x: Double, val y: Double) {
override def toString() = "Point(" + x + "," + y + ")"
}
abstract class Shape() {
def draw(): Unit
}
class Circle(val center: Point, val radius: Double) extends Shape {
def draw() = println("Circle.draw: " + this)
override def toString() = "Circle(" + center + "," + radius + ")"
}
}
This particular example is not the best since
Shape
should probably be atrait
, not anabstract 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 ofif/else
statements, something like:(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:
But this is actually so common an idea that it's built into the language. When we write something like
"under the hood" this is doing something very similar; it's creating a special value
Circle.class
which contains thisdraw()
andarea()
method. When you create an instance ofCircle
byval circle = new Circle()
, as well as the ordinary fieldscenter
andradius
, thisCircle
has a magic, hidden fieldcircle.__type = Circle.class
.When you call
shape.draw()
, this is sort of equivalent toshape.__type.draw(shape)
(not real syntax). Which is great, because it means that ifshape
is aSquare
, then the call will beSquare.class.draw(shape)
(again, not real syntax), but if it's aCircle
then the call will beCircle.class.draw(shape)
. Notice how a class always gets called with a value of the correct type (it's impossible to callSquare.class.draw(circle)
, becausecircle.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:and when I call
shape.draw()
, it will call the right thing. But if I have some other class: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:while this method won't compile:
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):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 todrawAll
, as long as it has adraw()
method.So that's where the
trait
comes in.means roughly "I promise that
Circle
has adef draw(): Unit
method. (Recall that this really means "I promiseCircle.class
contains a valuedraw: Circle => Unit
). The compiler will enforce your promise, refusing to compileCircle
if it doesn't implement the given methods. Then we can do:and the compiler requires that every
shape
inshapes
is from a type with adef draw(): Unit
method. Soshape.__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:
Rather than writing the same code twice, we can put it in a
trait
:Notice that this is the same thing we wrote in the previous case, and so we can also use it the same way:
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
extendsHasStomach
, we should compose aStomach
into ourHorse
: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 andabstract class
es (and to a certain extent also toclass
es, but let's not go into that).For many cases, both a
trait
and anabstract class
will work, and some people advise using the difference to declare intent: if you want to implement a common interface, use atrait
, and if you want to share implementation code, use anabstract 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: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, buttrait
s may not. A class may inherit from any number oftrait
s, but anabstract 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 useabstract class
for something that needs to have a constructor.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'sdraw
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 theShape
interface, so it is guaranteed to contain thedraw
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).
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 atakeMeToWork()
method on it you could calltakeMeToWork()
on anything that inherited from Transportation and expect to end up at work. You wouldn't know if you were taking aCar
or aBicycle
to work but you'd be going to work.Transportation
would only promise that there will be atakeMeToWork()
method. It wouldn't define how it works and in fact won't work until it is provided with aCar
orBicycle
that does.If you demand that every form of
Transportation
have the same cup holder for your drink you could put auseCupHolder()
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.