How would you explain Scala's abstract class feature to a 6th grader?
我试图从O'Reilly的编程scala中理解这个代码示例。我是一个JavaScript程序员,书中的大部分解释都是Java背景。我正在寻找一个抽象类及其用途的简单、高级解释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 +")" } } |
抽象的,没有太多细节的。这是一种正式的表达方式,"我们含糊其辞"。
说"我有一种上班的交通工具",比说"我有一辆上班的车"更抽象。当然,在某个地方,有些东西能确切知道你要干什么。这是因为不必在任何地方确切地知道什么。这个想法叫做抽象。
它是如何使用的:
在大多数OOP语言中,抽象类或父类是一个集中可重用的通用方法并为代码驻留在更具体或子类上的更指定方法提供接口的地方。
因此,如果我提供了一个名为
如果你要求每种形式的
欣赏这个比喻的问题在于,除非你处在一个有用的环境中,否则很难理解它的意义。现在听起来好像有很多花哨的、难以理解的东西,这些东西只会让解决任何问题变得更加困难。这是事实。在您发现自己处理的代码足够复杂以至于可以利用它并掌握抽象之前,它只会使事情变得更困难。但一旦你得到它,一切都会变得简单。尤其是当你不单独写代码的时候。下一个比喻不是经典的,但它是我最喜欢的:
为什么我们车上有引擎盖?
(或者给非美国人的小骨头)
没有它这辆车运行得很好。没有它,所有的酷引擎都更容易得到。那是为了什么?如果没有引擎盖,我可以坐在发动机上,在机架和销轴上插入一个投票,抓住油门,然后驾驶汽车。现在我可以做一些很酷的事情,比如以每小时50英里的速度换油。
这些年来,我们发现,如果不在脸上沾一根油尺,人们真的会更舒服地驾驶。所以我们把引擎盖放在车上,并提供了加热座椅、方向盘和油门踏板。这让我们感到舒适,防止我们的裤腿被风扇皮带缠住。
在软件中,我们用抽象提供相同的东西。它有许多形式、抽象类、特征、外观模式等,甚至卑微的方法也是一种抽象形式。
你解决的问题越复杂,使用一些明智的抽象就越好。而且,你的车带着引擎盖看起来更酷。
由于
继承做了两件独立但相关的事情:它允许不同的值实现一个公共接口,并允许不同的类共享实现代码。好的。公共接口
假设我们有一个绘图程序需要处理一系列不同的形状——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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中,我们实际上使用的是模式匹配,但暂时不要担心这个问题)好的。
这是一种重复的模式;最好隔离重复的"找出正确的形状类型,然后调用正确的方法"逻辑。我们可以自己编写这个想法(一个虚拟函数表):好的。
1 2 3 4 5 6 7 8 9 10 11 | 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) |
但这实际上是一个很常见的想法,它是建立在语言中的。当我们写一些像好的。
1 2 3 4 5 6 7 8 9 10 |
"引擎盖下"这是做一些非常相似的事情;它创造了一个特殊的价值
当您调用
现在,很多语言都有这样的东西,没有
1 2 3 4 | class Square: def draw(self): ... class Circle: def draw(self): ... |
当我给
1 | class Thursday: ... |
然后我可以调用
1 | def doSomething(s: Square): s.draw() |
虽然此方法无法编译:好的。
1 | def doSomething(t: Thursday): t.draw() |
scala的类型系统非常强大,您可以使用它来证明您的代码的所有方面,但至少,它保证的一个好处是"您永远不会调用不存在的方法"。但当我们想在未知形状类型上调用
1 |
但即使这不是我们真正想要的:如果有人写他们自己的
所以,这就是
大致意思是"我保证
编译器要求
(事实上,scala还有一个更强大的方法来达到同样的效果,即typeclass模式,但现在我们不必担心这个问题。)好的。共享实施
这更简单,但也更"凌乱"——这是一个纯粹的实际问题。好的。
假设我们有一些符合对象状态的公共代码。例如,我们可能有一群不同的动物可以吃东西:好的。
1 2 3 4 5 6 7 8 9 10 | class Horse { private var stomachContent: Double = ... def eat(food: Food) = { //calorie calculation stomachContent += calories } } class Dog { def eat(food: Food) = ... } |
我们可以把它放在一个
1 2 3 4 5 6 | trait HasStomach { var stomachContent: Double def eat(food: Food) = ... } class Horse extends HasStomach class Dog extends HasStomach |
请注意,这与我们在前一个案例中编写的内容相同,因此我们也可以用同样的方式使用它:好的。
1 |
但希望您能看到我们的意图是不同的;即使
有些人批评"传统的"OO继承,因为它"混合"了这两种含义。没有办法说"我只想共享这段代码,不想让其他函数调用它"。这些人倾向于认为共享代码应该通过组合来实现:与其说我们的
1 2 3 4 5 6 7 8 | class Stomach { val content: Double = ... def eat(food: Food) = ... } class Horse { val stomach: Stomach def eat(food: Food) = stomach.eat(food) } |
这种观点是有道理的,但在实践中(在我的经验中),它往往会导致比"传统的OO"方法更长的代码,特别是当您想要为大型、复杂的对象生成两种不同的类型,并且这两种类型之间存在一些微小的差异时。好的。抽象类与特征
到目前为止,我所说的一切都同样适用于
在许多情况下,
scala允许多重继承;一个类可以有多个父类:好的。
1 | class Horse extends HasStomach, HasLegs, ... |
这很有用,原因很明显,但在菱形继承情况下可能会有问题,特别是当您有调用超类方法的方法时。请参阅python的super-consided有害于python中出现的一些问题,并注意在实践中,大多数问题都发生在构造函数中,因为这些方法通常都希望调用超类方法。好的。
scala对此有一个很好的解决方案:
所以在实际代码中,我的建议是尽可能始终使用
抽象类只提供一个定义的接口和许多方法。抽象类的任何子类都可以看作是该类的特定实现或细化。
这允许您定义一个采用
在类型系统方面,请求
就个人而言,我更倾向于使用特征而不是抽象类,后者在斯卡拉中对我有一点Java气味。区别在于抽象类可能有构造函数参数。另一方面,具体的实现类可以自由地实现多个特性,而它只能扩展一个类(抽象的或非抽象的)。