8-组合和继承
前言
本节将以一个类库的形式,讨论抽象方法、无参方法、类的扩展、重写方法和字段、参数化字段、调用超类的构造方法等等。
环境:
Windows + Scala-2.12.8
代码见GitHub:
https://github.com/GYT0313/Scala-Learning
1. 一个二维的布局类库
将创建一个用于构建和渲染二维布局元素的类库,以此作为示例。将定义可以从数组、线和矩形构造出元素对象的类。以及定义组合操作符above 和 beside。这样的操作符通常被称为组合子,因为它们将在某个领域内的元素组合成新的元素。
2. 抽象类
首先定义Element 类型,用来表示元素。元素是一个由字符组成的二维矩形,用一个成员contents 来表示某个布局元素。定义:
10.1.scala
abstract class Element {
def contents: Array[String]
}
关键字abstract ,不能实例化一个抽象类,如:
与Java 一样,没有定义方法体的方法称为抽象方法,抽象方法必须存在于抽象类中。
3. 定义无参方法
定义两个方法来获取Element 的宽度和高度:
abstract class Element {
def contents: Array[String]
def height: Int = contents.length
def width: Int = if (height == 0) 0 else contents(0).length
}
因为每行宽度相同,所以获取第一行的宽度。
或许你会产生疑问,无参方法不是应该 def height(): Int 这样定义嘛。因为,def height: Int 在Scala中很常见,它称为无参方法,对应的def height(): Int 称为空圆括号方法。如果你是一个没有参数的方法,推荐使用无参方法,是为了支持所谓的统一访问原则。
你也可以理解为,无参方法和字段有相似之处,是为了返回一个值,所以在调用时无参方法和字段的方式相同,如:Element.height。
总之,Scala鼓励将那些不接收参数也没有副作用的方法定义为无参方法。对于有副作用的方法,不应该省去空括号。
4. 扩展类
我们已经定义了抽象类Element,不过它并不能被实例化,我们需要创建一个扩展自Element 的子类,并实现contents 这个抽象方法,如:
10.1.scala
class ArrayElement(conts: Array[String]) extends Element {
def contents: Array[String] = conts
}
这里ArrayElement 继承了Element 的所有非私有成员。并且重写了contents 方法。
上图中,ArrayElement 和Array[String] 的关系称为组合。
5. 重写方法和字段
统一访问原则只是Scala比Java 在处理字段和方法上更加统一的一个方面。另一个区别是Scala中字段和方法属于同一个命名空间。这使得字段重写无参方法变为可能。如,将contents 方法重写为 contents 变量:
10.1.scala
class ArrayElement(conts: Array[String]) extends Element {
val contents: Array[String] = conts
}
一般来说,Scala只有两个命名空间用于定义,不同于Java 的四个。Java 的四个命名空间是:字段、方法、类型和包。Scala 的两个为:
- 值(字段、方法、包和单例对象)
- 类型(类和特质名)
6. 定义参数化字段
ArrayElement 有一个conts 参数,唯一的用处就是拷贝到contents 字段上。有一种冗余和重复的信号,可以通过参数化字段来解决:
10.1.scala
class ArrayElement(
val contents: Array[String]
) extends Element
在参数contents 前面多了一个val 。
7. 调用超类构造方法
现在已经有了一个用Array[String] 构造的类,现在我们来定义一个String 的类,扩展自ArrayElement:
10.1.scala
class LineElement(s: String) extends ArrayElement(Array(s)) {
override def width = s.length
override def height = 1
}
新的类继承关系图:
8. 多态和动态绑定
学习过Java 应该知道什么是多态,比如:
目前为止,我们定义了ArrayElement和LineElement。还有一个矩阵构造,我们需要传递一个字符、宽度、高度,如:
10.1.scala
class UniformElement(
ch: Char,
override val width: Int,
override val height: Int
) extends Element {
private val line = ch.toString * width
def contents = Array.fill(height)(line)
}
目前为止,我们可以有如下实例化方式:
现在的类图:
9. 声明final 成员
跟Java 一样,如果你想要某个成员不能被子类继承在其前面写上final 即可,如:
class ArrayElement extends Element {
final override def demo() = println("hello")
}
final class ArrayElement extends Element {
override def demo() = println("hello")
}
10. 使用组合和继承
在前一个版本,将LineElement 定义为ArrayElement 的子类的主要目的是复用ArrayElement的contents 定义。因此,更好的做法是将LineElement 定义为Element 的直接之类,如:
10.1.scala
class LineElement(s: String) extends Element {
val contents = Array(s)
override def width = s.length
override def height = 1
}
现在的类图:
现在,ArrayElement 和LineElement 都和Array[String] 有组合关系。
11. 实现above、beside和toString
接下来将实现Element 类的above 和 beside 方法。above方法将两个元素上下重叠返回一个新的元素,第一版的above 可能是这样:
10.1.scala
def above(that: Element): Element =
new ArrayElement(this.contents ++ that.contents)
现在只能处理两个相同宽度的元素,不过先不管这个问题。下一个方法beside,将两个元素左右拼接放回新的元素。第一版方法:
10.1.scala
def beside(that: Element): Element = {
val contents = new Array[String](this.contents.length)
for (i <- 0 until this.contents.length)
contents(i) = this.contents(i) + that.contents(i)
new ArrayElement(contents)
}
不过上述beside 是一个指令式风格的代码,换一种更加简单的方式:
def beside(that: Element): Element = {
new ArrayElement(
for (
(line1, line2) <- this.contents zip that.contents
) yield line1 + line2
)
}
这里使用了zip操作符,将两个数组转换成对偶的数组,如:
长度过长的将被舍去。另外,for 表达式 和yield 将(line1, line2) 作为Array[String] 返回。
现在,还需要某种方式来显示元素,这里重写toString 方法:
override def toString = contents mkString "\n"
例如:
所以,我们现在的Element 类看上去就像这样:
10.1.scala
abstract class Element {
def contents: Array[String]
def width: Int =
if (height == 0) 0 else contents(0).length
def height: Int = contents.length
def above(that: Element): Element =
new ArrayElement(this.contents ++ that.contents)
def beside(that: Element): Element =
new ArrayElement(
for (
(line1, line2) <- this.contents zip that.contents
) yield line1 + line2
)
override def toString = contents mkString "\n"
}
12. 定义工厂对象
工厂对象包含创建其他对象的方法。使用方 用些方法构造对象,而不是直接用new 构造对象。这种做法的好处是对象创建逻辑可以被集中起来,而对象是如何使用具体的类表示的可以被隐藏起来。
这里使用Element 类的伴生对象来构建,包含了三个重载的elem方法:
10.2.scala
object Element {
def elem(contents: Array[String]): Element =
new ArrayElement(contents)
def elem(chr: Char, width: Int, height: Int): Element =
new UniformElement(chr, width, height)
def elem(line: String) =
new LineElement(line)
}
有了工厂方法后,对Element 类做一些改变,让它使用elem 工厂方法,而不是直接显示地创建新的ArrayElement。为了在调用工厂方法时不显示给出Element 这个单例对象的限定词,我们将在源文件顶部引入Element.elem。可以更加简单的直接使用elem,调整后的Element 类:
10.2.scala
import Element.elem
abstract class Element {
def contents: Array[String]
def width: Int =
if (height == 0) 0 else contents(0).length
def height: Int = contents.length
def above(that: Element): Element =
elem(this.contents ++ that.contents)
def beside(that: Element): Element =
elem(
for (
(line1, line2) <- this.contents zip that.contents
) yield line1 + line2
)
override def toString = contents mkString "\n"
}
同时,有了工厂方法ArrayElement、LineElement、UniformElement可以变成私有的,因为它们不需要被使用方 直接访问了。修改单例对象:
10.2.scala
object Element {
private class ArrayElement(
val contents: Array[String]
) extends Element
private class UniformElement(
ch: Char,
override val width: Int,
override val height: Int
) extends Element {
private val line = ch.toString * width
def contents = Array.fill(height)(line)
}
private class LineElement(s: String) extends Element {
val contents = Array(s)
override def width = s.length
override def height = 1
}
def elem(contents: Array[String]): Element =
new ArrayElement(contents)
def elem(chr: Char, width: Int, height: Int): Element =
new UniformElement(chr, width, height)
def elem(line: String) =
new LineElement(line)
}
到目前为止,我们可以测试一下类库:
Element-Test.scala
object Element {
private class ArrayElement(
val contents: Array[String]
) extends Element
private class UniformElement(
ch: Char,
override val width: Int,
override val height: Int
) extends Element {
private val line = ch.toString * width
def contents = Array.fill(height)(line)
}
private class LineElement(s: String) extends Element {
val contents = Array(s)
override def width = s.length
override def height = 1
}
def elem(contents: Array[String]): Element =
new ArrayElement(contents)
def elem(chr: Char, width: Int, height: Int): Element =
new UniformElement(chr, width, height)
def elem(line: String): Element =
new LineElement(line)
}
import Element.elem
abstract class Element {
def contents: Array[String]
def width: Int =
if (height == 0) 0 else contents(0).length
def height: Int = contents.length
def above(that: Element): Element =
elem(this.contents ++ that.contents)
def beside(that: Element): Element =
elem(
for (
(line1, line2) <- this.contents zip that.contents
) yield line1 + line2
)
override def toString = contents mkString "\n"
}
object Test {
def main(args: Array[String]) {
// ArrayElement
println(elem(Array("hello", "world")).toString + "\n")
// LineElement
println(elem("hello").toString + "\n")
// UniformElement
println(elem('x', 4, 2).toString + "\n")
// above beside
val column1 = elem("hello") above elem("***")
val column2 = elem("***") above elem("world")
println(column1 beside column2)
}
}
运行:
13. 增高和增宽
最后我们还需要怎加将不同高度、宽度整齐放置的功能,我们在抽象类中定义widen 和 heighten 方法:
为增加之前可能是这样的:
修改后的 Element.scala:
object Element {
private class ArrayElement(
val contents: Array[String]
) extends Element
private class UniformElement(
ch: Char,
override val width: Int,
override val height: Int
) extends Element {
private val line = ch.toString * width
def contents = Array.fill(height)(line)
}
private class LineElement(s: String) extends Element {
val contents = Array(s)
override def width = s.length
override def height = 1
}
def elem(contents: Array[String]): Element =
new ArrayElement(contents)
def elem(chr: Char, width: Int, height: Int): Element =
new UniformElement(chr, width, height)
def elem(line: String): Element =
new LineElement(line)
}
import Element.elem
abstract class Element {
def contents: Array[String]
def width: Int =
if (height == 0) 0 else contents(0).length
def height: Int = contents.length
def above(that: Element): Element = {
// above 只需要控制左右居中
val this1 = this widen that.width
val that1 = that widen this.width
elem(this1.contents ++ that1.contents)
}
def beside(that: Element): Element = {
// beside 只需要控制上下居中
val this1 = this heighten that.height
val that1 = that heighten this.height
elem(
for (
(line1, line2) <- this1.contents zip that1.contents
) yield line1 + line2
)
}
def widen(w: Int): Element =
// 如果w <= width, 直接返回
if (w <= width) this
else {
// left 和 right 保证短的元素左右居中
val left = elem(' ', (w - width) / 2, height)
val right = elem(' ', w - width - left.width, height)
left beside this beside right
}
def heighten(h: Int): Element =
if (h < height) this
else {
// top 和 bot 保证短的元素上下居中
val top = elem(' ', width, (h - height) / 2)
val bot = elem(' ', width, h - height - top.height)
top above this above bot
}
override def toString = contents mkString "\n"
}
object Test {
def main(args: Array[String]) {
val str1 = "Here is Element.scala"
val str2 = "Using scala"
val str3 = "Combination and inheritance"
println(elem(str1) above elem(str2) above elem(str3))
}
}
再次运行:
14. 放在一起
练习使用布局类库的所有几乎元素的趣味方式是编写一个用给定的边数绘制螺旋程序(运行过程很复杂,文章最后给出了调试流程(使用IDEA,代码见:Spiral-DEBUG.scala)):
Spiral.scala
import Element.elem
object Spiral {
val space = elem(" ")
val corner = elem("+")
def spiral(nEdges: Int, direction: Int): Element = {
if (nEdges == 1)
elem("+")
else {
val sp = spiral(nEdges - 1, (direction + 3) % 4)
def verticalBar = elem('|', 1, sp.height)
def horizontalBar = elem('-', sp.width, 1)
if (direction == 0)
(corner beside horizontalBar) above (sp beside space)
else if (direction == 1)
(sp above space) beside (corner above verticalBar)
else if (direction == 2)
(space beside sp) above (horizontalBar beside corner)
else
(verticalBar above corner) beside (space above sp)
}
}
def main(args: Array[String]) = {
val nSides = args(0).toInt
println(spiral(nSides, 0))
}
}
object Element {
private class ArrayElement(
val contents: Array[String]
) extends Element
private class UniformElement(
ch: Char,
override val width: Int,
override val height: Int
) extends Element {
private val line = ch.toString * width
def contents = Array.fill(height)(line)
}
private class LineElement(s: String) extends Element {
val contents = Array(s)
override def width = s.length
override def height = 1
}
def elem(contents: Array[String]): Element =
new ArrayElement(contents)
def elem(chr: Char, width: Int, height: Int): Element =
new UniformElement(chr, width, height)
def elem(line: String): Element =
new LineElement(line)
}
abstract class Element {
def contents: Array[String]
def width: Int =
if (height == 0) 0 else contents(0).length
def height: Int = contents.length
def above(that: Element): Element = {
// above 只需要控制左右居中
val this1 = this widen that.width
val that1 = that widen this.width
elem(this1.contents ++ that1.contents)
}
def beside(that: Element): Element = {
// beside 只需要控制上下居中
val this1 = this heighten that.height
val that1 = that heighten this.height
elem(
for (
(line1, line2) <- this1.contents zip that1.contents
) yield line1 + line2
)
}
def widen(w: Int): Element =
// 如果w <= width, 直接返回
if (w <= width) this
else {
// left 和 right 保证短的元素左右居中
val left = elem(' ', (w - width) / 2, height)
val right = elem(' ', w - width - left.width, height)
left beside this beside right
}
def heighten(h: Int): Element =
if (h < height) this
else {
// top 和 bot 保证短的元素上下居中
val top = elem(' ', width, (h - height) / 2)
val bot = elem(' ', width, h - height - top.height)
top above this above bot
}
override def toString = contents mkString "\n"
}
运行:
这里给出6 边螺旋的调试信息:(代码见Spiral-DEBUG.scala)
其实执行流程带过于复杂(使用了递归),只当练习一下即可。
Connected to the target VM, address: '127.0.0.1:57202', transport: 'socket'
+-
+
+-+
+ |
|
+-+
+ |
|
---+
|
| +-+
| + |
| |
+---+
+-----
|
| +-+
| + |
| |
+---+
+-----
|
| +-+
| + |
| |
+---+