Facebook Twitter LinkedIn E-mail
magnify
Home 2013 十一月

Scala开发教程(42): 组合和继承–定义heighten和widen函数

我们还需要最后一个改进,之前的Element实现不够完善,只支持同样高度和同样宽度的Element使用above和beside函数,比如下面的代码将无法正常工作,因为组合元素的第二行比第一行要长:

new ArrayElement(Array("hello")) above 
new ArrayElement(Array("world!"))

与之相似的,下面的表达式也不能正常工作,因为第一个ArrayElement高度为二,而第二个的高度只是一

new ArrayElement(Array("one", "two")) beside 
new ArrayElement(Array("one"))

下面代码展示了一个私有帮助方法,widen,能够带个宽度做参数并返回那个宽度的Element。结果包含了这个Element的内容,居中,左侧和右侧留需带的空格以获得需要的宽度。这段代码还展示了一个类似的方法,heighten,能在竖直方向执行同样的功能。widen方法被above调用以确保Element堆叠在一起有同样的宽度。类似的,heighten方法被beside调用以确保靠在一起的元素具有同样的高度。有了这些改变,布局库函数可以使用了

abstract class Element {
  def contents: Array[String]
  def height: Int = contents.length
  def width: Int = if (height == 0) 0 else contents(0).length
  def above(that: Element) :Element =
    Element.elem(this.contents ++ that.contents)
  def beside(that: Element) :Element = {
    Element.elem(
      for( 
        (line1,line2) <- this.contents zip that.contents
      ) yield line1+line2
    ) 
  }
  def widen(w: Int): Element =
  if (w <= width) this
  else {
    val left = Element.elem(' ', (w - width) / 2, height)
        var right = Element.elem(' ', w - width - left.width, height)
        left beside this beside right
  } 
 
def heighten(h: Int): Element =
  if (h <= height) this  
  else {  
    val top = Element.elem(' ', width, (h - height) / 2)
        var bot = Element.elem(' ', width, h - height - top.height)
        top above this above bot
  }  
  override def toString = contents mkString "\n"

}
 

Scala开发教程(41): 组合和继承–定义factory对象

到目前为止,我们定义了关于布局元素类的一个层次结构。你可以把包含这个层次关系的类作为API接口提供给其它应用,但有时你可以希望对函数库的用户隐藏这种层次关系,这通常可以使用factory(构造工厂)对象来实现。一个factory对象定义了用来构造其它对象的函数。库函数的用户可以通过工厂对象来构造新对象,而不需要通过类的构造函数来创建类的实例。使用工厂对象的好处是,可以统一创建对象的接口并且隐藏被创建对象具体是如何来表示的。这种隐藏可以使得你创建的函数库使用变得更简单和易于理解,也正是隐藏部分实现细节,可以使你有机会修改库的实现而不至于影响库的接口。
实现factory对象的一个基本方法是采用singleton模式,在Scala中,可以使用类的伴随对象(companion 对象)来实现。比如:

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) :Element =
    new LineElement(line)
}

我们先把之前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 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"

}

有了object Element(类Element的伴随对象),我们可以利用Element对象提供的factory方法,重新实现类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 above(that: Element) :Element =
    Element.elem(this.contents ++ that.contents)
  def beside(that: Element) :Element = {
    Element.elem(
      for( 
        (line1,line2) <- this.contents zip that.contents
      ) yield line1+line2
    ) 
  }
  override def toString = contents mkString "\n"

}

这里我们重写了above和beside方法,使用伴随对象的factory方法Element.elem替代new 构造函数。
这样修改之后,库函数的用户不要了解Element的继承关系,甚至不需要知道类ArrayElement,LineElement定义的存在,为了避免用户直接使用ArrayElement或LineElement的构造函数来构造类的实例,因此我们可以把ArrayElement,UniformElement和LineElement 定义为私有,定义私有可以也可以把它们定义在类Element内部(嵌套类)。下面为这种方法的使用:

object Element {

  private class ArrayElement(val contents: Array[String])
    extends Element {
  }

  private class LineElement(s:String) extends ArrayElement(Array(s)) {
    override def width = s.length
    override def height = 1
  }

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

 

Scala开发教程(40): 组合和继承–实现类Element的above,beside和toString()方法

我们接着实现类Element的其它方法,如above, beside和toString方法。
above方法,意味着把一个布局元素放在另外一个布局元素的上方,也就是把这两个元素的contents的内容连接起来。我们首先实现above函数的第一个版本:

def above(that: Element) :Element =
    new ArrayElement(this.contents ++ that.contents)

Scala中Array 使用Java Array来实现,但添加很多其它方法,尤其是Scala中Array可以转换为scala.Seq类的实例对象,scala.Seq为一个序列结构并提供了许多方法来访问和转换这个序列。
实际上上面above的实现不是十分有效,因为它不允许你把不同长度的布局元素叠加到另外一个布局元素上面,但就目前来说,我们还是暂时使用这个实现,只使用同样长度的布局元素,后面再提供这个版本的增强版本。
下面我们再实现类Element的另外一个beside方法,把两个布局元素并排放置,和前面一样,为简单起见,我们暂时只考虑相同高度的两个布局元素:

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

这里我们使用了Array的zip 操作符,可以用来将两个数组转换成二元组的数组,zip 分别取两个数组对应的元素组成一个新的二元祖,比如:

scala> Array( 1,2,3) zip Array("a","b")
res0: Array[(Int, String)] = Array((1,a), (2,b))

如果一个数组长度大于另外一个数组,多余的元素被忽略。 for 的yield部分用来构成一个新元素。

最后我们实现Element的toString方法用来显示布局元素的内容:

override def toString = contents mkString "\n"

这里使用mkString函数,这个函数可以应用到任何序列数据结构(包括数组),也就是把contents的每个元素调用toString,然后使用”\n”分隔。

 

Scala开发教程(39): 组合和继承–使用组合还是继承?

前面我们说过,构建新类的两个基本方法是组合和继承,如果你的主要目的是代码重用,那么最好使用组合的方法构造新类,使用继承的方法构造新类造成的可能问题是,无意的修改基类可能会破坏子类的实现。
关于继承关系你可以问自己一个问题,是否它建模了一个is-a关系。例如,说ArrayElement是Element是合理的。你能问的另一个问题是,是否客户想要把子类类型当作基类类型来用。
前一个版本中,LineElement与ArrayElement有一个继承关系,从那里继承了contents。现在它在ArrayElement的例子里,我们的确期待客户会想要把ArrayElement当作Element使用。
请看下面的类层次关系图:
20131120001

看着这张图,问问上面的问题,是否感觉其中的任何关系有可疑吗?尤其是,对你来说LineElement是ArrayElement是否显而易见呢?你是否认为客户会需要把LineElement当作ArrayElement使用?实际上,我们把LineElement定义为ArrayElement主要是想重用ArrayElement的contents定义。因此或许把LineElement定义为Element的直接子类会更好一些,就像这样:

class LineElement(s: String) extends Element { 
  val contents = Array(s) 
  override def width = s.length 
  override def height = 1 
}

前一个版本中,LineElement与ArrayElement有一个继承关系,从那里继承了contents。现在它与Array有一个组合关系:在它自己的contents字段中持有一个字串数组的引用。有了LineElement的这个实现,Element的继承层级现在如下图所示:

20131124001

因此在选用组合还是通过继承来构造新类时,需要根据需要选择合适的方法。

 

Scala开发教程(38): 组合和继承–定义final成员

在定义类的继承关系时,有时你不希望基类的某些成员被子类重载,和Java类似,在Scala中也是使用final来修饰类的成员。比如在前面的ArrayElement例子,在demo方法前加上final修饰符,

class ArrayElement extends Element { 
  final override def demo() { 
    println("ArrayElement's implementation invoked") 
  } 
} 

如果LineElement试图重载demo,则会报错:

scala> class LineElement extends ArrayElement { 
     |   override def demo() { 
     |     println("LineElement's implementation invoked")
     |   }
     | 
     | } 
<console>:10: error: overriding method demo in class ArrayElement of type ()Unit;
 method demo cannot override final member
         override def demo() { 

如果你希望某个类不可以派生子类,则可以在类定义前加上final修饰符:

final class ArrayElement extends Element { 
   override def demo() { 
    println("ArrayElement's implementation invoked") 
  } 
} 

此时如果还是重载LineElement的demo函数,则会报错:

scala> class LineElement extends ArrayElement { 
     |   override def demo() { 
     |     println("LineElement's implementation invoked")
     |   }
     | 
     | } 
<console>:9: error: illegal inheritance from final class ArrayElement
       class LineElement extends ArrayElement { 

 

Scala开发教程(37): 组合和继承–多态和动态绑定

在前面的例子我们看到类型为Element的变量可以保存ArrayElement类型的对象,这种现象称为“多态”。也就是基类类型的变量可以保存其子类类型的对象,到目前为止我们定义了两个Element的子类,ArrayElement和LineElement。你还可以定义其它子类,比如:

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

结合前面定义的类定义,我们就有了如下图所示的类层次关系:

20131120001

Scala将接受所有的下列赋值,因为赋值表达式的类型符合定义的变量类型:

val e1: Element = new ArrayElement(Array("hello", "world")) 
val ae: ArrayElement = new LineElement("hello") 
val e2: Element = ae val 
e3: Element = new UniformElement('x', 2, 3)

若你检查继承层次关系,你会发现这四个val定义的每一个表达式,等号右侧表达式的类型都在将被初始化的等号左侧的val类型的层次之下。
另一方面,如果调用变量(对象)的方法或成员变量,这个过程是一个动态绑定的过程,也就是说调用哪个类型的方法取决于运行时变量当前的类型,而不是定义变量的类型。
为了显示这种行为,我们在Element中添加一个demo方法,定义如下:

abstract class Element { 
  def demo() { 
    println("Element's implementation invoked") 
  } 
} 

class ArrayElement extends Element { 
  override def demo() { 
    println("ArrayElement's implementation invoked") 
  } 
} 

class LineElement extends ArrayElement { 
  override def demo() { 
    println("LineElement's implementation invoked")
  }

} 

// UniformElement inherits Element’s demo 
class UniformElement extends Element

如果你使用交互式Scala解释器来测试,你可以定义如下的方法:

def invokeDemo(e: Element) { 
  e.demo() 
}

下面我们分别使用ArrayElement, LineElement和UniformElement来调用这个方法:

scala> invokeDemo(new ArrayElement)
ArrayElement's implementation invoked

scala> invokeDemo(new LineElement)
LineElement's implementation invoked

scala> invokeDemo(new UniformElement)
Element's implementation invoked

可以看到由于ArrayElement和LineElement重载了Element的demo方法,因此调用invokeDemo时由于“动态绑定”因此会调用这些子类的demo方法,而由于UniformElement没有重载Element的demo方法,动态绑定时也会调用UniformElement的demo方法(但此时实际为基类的demo方法)