您如何向六年级学生解释Scala的抽象课程特征?

时间:2014-10-25 21:05:09

标签: java class scala oop

我试图理解来自O' Reilly编程Scala的代码示例。我是一名JavaScript程序员,本书中的大多数解释都假设是Java背景。我正在寻找一个简单的,高级别的抽象类解释以及它们用于什么。

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

3 个答案:

答案 0 :(得分:4)

抽象如同没有太多细节。这是一种说“我们模糊不清”的正式方式。

说,“我有一种交通方式可供我上班。”更抽象的是,“我有一辆车可以上班”。当然,某些地方确实知道你正在做什么。这无需知道到处都是什么。这个想法叫做抽象。

如何使用:

大多数OOP语言中的抽象或父类是集中可重用通用方法的地方,并为更多指定方法提供接口,其代码驻留在更具体或子类上。

因此,如果我提供了一个名为Transportation的抽象类,其中包含takeMeToWork()方法,则可以对从Transport继承的任何内容调用takeMeToWork(),并期望最终开始工作。你不会知道你是在做Car还是Bicycle工作但是你会去上班。 Transportation只会承诺会有takeMeToWork()方法。它不会定义它是如何工作的,实际上只有提供CarBicycle才会起作用。

如果你要求每种形式的Transportation都有相同的杯架,你可以在运输课上放一个useCupHolder()方法,而不必再写一次。它总是以完全相同的方式工作。取决于接口或“特征”可能无法使用的语言或语言版本。除了提供默认实现之外,抽象类与特征没有太大区别。 This question处理这些差异。


欣赏这个比喻的问题是,在你证明有用的情况之前,很难看出这一点。现在看起来很可能听起来很难理解那些只能解决任何问题的东西。这确实是事实。直到你发现自己使用足够复杂的代码来利用这个和掌握的抽象,它才会让事情变得更难。一旦你得到它虽然它使一切更容易。特别是当你不单独编写代码时。下一个比喻不是经典的,但它是我最喜欢的:

为什么我们在汽车上有引擎盖?

(或bonets为非美国人)

没有它,汽车运行良好。如果没有它,所有酷炫的引擎都会更容易实现。那有什么用呢?如果没有发动机罩,我可以坐在发动机缸体上,将一个调查结果塞进机架和销钉,抓住油门,然后开动汽车。现在我可以做很酷的事情,比如以每小时50英里换油。

多年来,我们发现人们在没有蘸棒的情况下驾驶更舒服。因此,我们将引擎盖放在汽车上,并提供加热座椅,方向盘和汽油兜售。这使我们感到舒适并且阻止我们将裤腿夹在风扇带中。

在软件中,我们提供与抽象相同的东西。它有多种形式,抽象类,特征,外观模式等。即使是简陋的方法也是一种抽象形式。

你解决的问题越复杂,你使用一些明智的抽象就越好。而且,你的车看起来比较凉爽。

答案 1 :(得分:2)

抽象类只提供一个定义的接口,一些方法。抽象类的任何子类都可以被视为该类的特定实现或细化。

这允许您定义一个采用Shape参数的方法,然后该方法的主体可以使用该接口,例如调用形状的draw方法,无论给出的形状类型如何。

就类型系统而言,要求Shape确保静态(在编译时)您只能传递满足Shape接口的对象,因此保证包含draw {1}}方法。


就个人而言,我更喜欢使用 traits 而不是抽象类,后者在Scala中有一点Java味道。不同之处在于抽象类可能具有构造函数参数。另一方面,具体的实现类可以自由地实现多个特征,而它只能扩展一个类(抽象或不抽象)。

答案 2 :(得分:2)

此特定示例不是最好的,因为Shape应该是trait,而不是abstract class

继承执行两个独立但相关的事情:它允许不同的值实现公共接口,并且它允许不同的类共享实现代码

通用接口

假设我们有一个绘图程序需要使用一堆不同形状的东西 - SquareCircleEquilateralTriangle等等。在过去的糟糕时期,我们可能会使用一堆if/else语句执行此操作,例如:

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

(在Scala中,我们实际上使用了模式匹配,但暂时不要担心这一点)

这是一种重复的模式;将重复的&#34;找出正确的形状类型,然后调用正确的方法&#34;逻辑。我们可以自己编写这个想法(虚拟函数表):

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)

但这实际上是一种常见的想法,它已经融入了语言。当我们写类似

的东西时
trait Shape {
  def draw(): Unit
  def area(): Double
}
class Circle extends Shape {
  val center: (Double, Double)
  val radius: Double
  def draw() = {...}
  def area() = {...}
}

&#34;引擎盖下&#34;这是做非常相似的事情;它创建了一个包含此Circle.classdraw()方法的特殊值area()。当您按Circle创建{{1>}的实例时,以及普通的字段 val circle = new Circle()center,这个radius有一个神奇的隐藏字段Circle

当你致电circle.__type = Circle.class时,这有点等同于shape.draw()(不是真正的语法)。这很棒,因为这意味着如果shape.__type.draw(shape)shape,则调用将是Square(再次,不是真正的语法),但如果它是{{1}然后调用将是Square.class.draw(shape)。注意如何使用正确类型的值调用类(它不可能调用Circle,因为Circle.class.draw(shape)总是进行正确的实现。)

现在,很多语言在没有Square.class.draw(circle)部分的情况下都有类似的东西。例如,在Python中,我可以这样做:

circle.draw()

当我拨打trait时,它会调用正确的内容。但如果我有其他课程:

class Square:
  def draw(self): ...
class Circle:
  def draw(self): ...

然后我可以调用shape.draw(),我会在运行时收到错误。 Scala是类型安全的语言(或多或少):此方法可以正常工作:

class Thursday: ...

虽然这种方法不会编译:

new Thursday().draw()

Scala的类型系统非常强大,你可以用它来证明你的代码的各种各样的东西,但至少,它保证的一个好处是&#34;你永远不会叫不存在的方法&#34;。但是当我们想要在未知类型的形状上调用我们的def doSomething(s: Square): s.draw() 方法时,这会产生一些问题。在某些语言中(例如,我相信Ceylon),您实际上可以编写这样的方法(无效的Scala语法):

def doSomething(t: Thursday): t.draw()

但即使这不是我们想要的:如果有人写自己的draw()课程,我们希望能够将其包含在我们传递给{{1}的列表中},只要它有def drawAll(shapes: List[Circle or Square or EquilateralTriangle]) = ... 方法

这就是Star进来的地方。

drawAll

大致意味着&#34;我保证draw()trait方法。 (回想一下,这确实意味着&#34;我保证trait Shape { def draw(): Unit def area(): Double } class Circle extends Shape {...} 包含值Circle)。编译器将强制执行您的承诺,如果它没有实现给定的方法,则拒绝编译def draw(): Unit。然后我们可以做到:

Circle.class

并且编译器要求draw: Circle => Unit中的每个Circle都来自具有def drawAll(shapes: List[Shape]) = ... 方法的类型。所以shape是&#34;安全&#34;,我们的方法保证只调用实际存在的方法。

(实际上Scala也有更强大的方法来实现相同的效果,类型类型,但现在不要担心。)

分享实施

这更简单,但也更容易&#34; - 这是一个纯粹实用的东西。

假设我们有一些与对象状态相关的公共代码。例如,我们可能有一堆不同的动物可以吃东西:

shapes

我们可以将它放在def draw(): Unit

中,而不是两次编写相同的代码
shape.__type.draw(shape)

请注意,这与我们在前一种情况下编写的内容相同,因此我们也可以使用相同的方式:

class Horse {
  private var stomachContent: Double = ...
  def eat(food: Food) = {
     //calorie calculation
     stomachContent += calories
  }
}
class Dog {
  def eat(food: Food) = ...
}

但希望你能看到我们的意图是不同的;即使trait是&#34;内部&#34;我们也可以做同样的事情。任何外部函数都无法调用的方法。

有些人批评&#34;传统&#34; OO继承,因为它&#34;混合&#34;这两个含义。没有办法说&#34;我只想分享这些代码,我不想让其他功能称之为&#34;。这些人倾向于认为共享代码应该通过组合发生:而不是说我们的trait HasStomach { var stomachContent: Double def eat(food: Food) = ... } class Horse extends HasStomach class Dog extends HasStomach 扩展 def feed(allAnimals: List[HasStomach]) = for {animal <- allAnimals} ... ,我们应该撰写 eat Horse

HasStomach

这种观点有一定的道理,但在实践中(根据我的经验),它往往导致代码比传统的OO&#34;方法,特别是当你想为一个大的,复杂的对象制作两种不同的类型时,两种类型之间有一些细微的差别。

抽象类与特征

到目前为止,我所说的所有内容同样适用于StomachHorse es(在某种程度上同样适用于class Stomach { val content: Double = ... def eat(food: Food) = ... } class Horse { val stomach: Stomach def eat(food: Food) = stomach.eat(food) } es,但不要进入那个)。

在很多情况下,traitabstract class都可以使用,有些人建议使用差异来声明意图:如果你想实现常用界面,使用class,如果您想共享实施代码,请使用trait。但在我看来,最重要的区别在于构造函数多重继承

Scala允许多重继承;一个班级可以abstract class几个家长:

trait

由于显而易见的原因,这很有用,但在钻石继承的情况下会出现问题,特别是当你有调用超类方法的方法时。有关Python中出现的一些问题,请参阅Python's Super Considered Harmful,并注意到在实践中,大多数问题都发生在构造函数中,因为这些是通常想要调用超类方法的方法

Scala有一个优雅的解决方案:abstract class es可能有构造函数,但extend可能没有。一个类可以从任意数量的class Horse extends HasStomach, HasLegs, ... 继承,但abstract class必须是第一个父类。这意味着任何类只有一个带有构造函数父类,所以它总是很明显哪个方法是&#34;超类构造函数&#34;。

所以在实际代码中,我的建议是尽可能使用trait,并且只对需要构造函数的东西使用trait