How would you explain Scala's abstract class f

2019-07-15 13:27发布

问题:

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 + ")"
    }
}

回答1:

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 traits and abstract classes (and to a certain extent also to classes, 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 classes may have constructors, but traits may not. A class may inherit from any number of traits, 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 traits where possible, and only use abstract class for something that needs to have a constructor.



回答2:

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.



回答3:

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).