LL脳がscalaの勉強を始めたよ その32


Scalaコップ本の10章を進めていきますよー、10章では2Dレイアウトライブラリーの実装の中でオブジェクト指向的なアレやコレを学んでいく感じですよー

とりあえず前回までの成果はこちらですー

// 抽象クラスElementを定義
abstract class Element {
  def contents:Array[String]
  def height:Int = contents.length
  def width:Int = if(height == 0) 0 else contents(0).length
}

// 配列を基にElementを構築しますよー
class ArrayElement(val contents:Array[String]) extends Element

// 文字列を基にElementを構築しますよ
class LineElement(s:String) extends Element{
  val contents = Array(s)
  override def width = s.length
  override def height = 1
}

// 特定範囲を指定文字で埋め尽くしたElementを構築しますよ
class UniformElement(
  ch:Char,
  override val width:Int,
  override val height:Int
 ) extends Element {
  private val line = ch.toString * width
  def contents = Array.make(height, line)
}

今回は実際の機能をガスガス追加していきますよー

above、beside、toStringの実装

とりあえず要素を上に載せたり横においたり文字列にするようなメソッドを定義していきますよー

aboveメソッド

2つの2Dレイアウト要素を重ねる(上に載せる)メソッドを実装しますよー

def above(that: Element): Element =  {
  // ++操作で配列を結合したものを基にElementオブジェクトを生成
  new ArrayElement(this.contents ++ that.contents)
}

上記のメソッド定義は単純なもののためwidthの値が異なる要素を重ねられない(要素である矩形が生成できないってことかしら?)ので、あとで異なるwidthでも使えるように拡張するとのことデス

それとScalaの配列はscala.Seqの継承なのでシーケンス的な構造らしいデスネ

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)
  // 生成した配列を基にElementオブジェクトを生成
  new ArrayElement(contents)
}

これもaboveメソッドと同じで各Element(要素の配列)のheight(要素数)が同じじゃないとダメな構造ですね…こいつもあとで拡張しますよっと。

ついでに上記メソッドは配列添字ループという命令型全開のコードになってしまっているので関数型風味に変更しますよ

def beside(that: Element):Element = {
  new ArrayElement(
    for (
      // zip演算子を使ってペア配列を作りますよー
      (line1, line2) <- this.contents zip that.contents
     // 各ペア配列を結合した要素をもつ配列を生成しマス
    ) yield line1 + line2
  )
} 

配列の添字操作はいつもドキドキなので、こういう形式だと一安心ですな

ちなみにzip演算子の動作はこんな感じみたいです

scala> Array(1,2) zip Array("a", "b")
// 2つの配列からペア配列を生々しますよ
res0: Array[(Int, java.lang.String)] = Array((1,a), (2,b))

ただし、配列の要素数が違う場合は余ったのを捨てちゃうみたいですねー

scala> Array(1,2) zip Array("a", "b", "c")
// 余ったcが捨てられましたよ
res1: Array[(Int, java.lang.String)] = Array((1,a), (2,b))
toStringメソッド

操作関連をある程度作ったので要素を表示するメソッドを定義しますよー

配列の要素一つ一つを一行ずつ表示するようにtoStringを上書きしますね

// 配列を文字列表示するtoStringをmkString処理でオーバーライド
override def toString = contents mkString "\n"

ちなみに上のメソッドは省略しまくりで不安になるので省略なしバージョンも

override def toString = contents.mkString("\n")
んじゃ、メソッドをElementに組み込みますよー

組み込み結果ですよー

// 抽象クラス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")
}

こんな感じで出来ましたよー

ファクトリーオブジェクトの定義

Element型にファクトリーメソッドパターンを導入しますよー

ファクトリーメソッドパターンを適用することでライブラリを利用するクライアントからクラスの階層構造を隠蔽してやりマス。これによってクライアントには(内部構造を意識しないですむ)使いやすさを、ライブラリ作成者には(クライアントコードを破壊しないような)内部修正のやりやすさを実現できる…はずデス…とのことです。

隠蔽方法としてはファクトリーメソッドをクラスやシングルトンオブジェクトに置く…etc...などなどの方法があるらしいのですが、今回はコップ本一押しのコンパニオンオブジェクト方式を採用しますよー

とりあえずArrayElement, lineElement, UniformElementの3種類を隠蔽するためのコンパニオンオブジェクトを定義しマスです

object Element {
  // ArrayElementの呼び出し用
  def elem(contents:Array[String]):Element = {
    new ArrayElement(contents)
  }
  // UniformElementの呼び出し用
  def elem(char:Char, width:Int, height:Int):Element = {
    new UniformElement(char, width, height)
  }
  // LineElementの呼び出し用
  def elem(line:String):Element = {
    new LineElement(line)
  }
}

Scalaでは引数の異なる同名のメソッドが定義できるのでこいつを使って各メソッドの呼出を隠蔽してしまいます。これによってクライアントは何のメソッドを呼び出すか(何を引数として渡すのか)意識せずともElementオブジェクトを作成できるようになるわけですねー

各メソッドの非公開的隠蔽

コンパニオンオブジェクトをElementクラスに組み込みますよー

まずはArrayElement, lineElement, UniformElementの3種類はクライアントから隠蔽してしまうので非公開にしてしまいますね、なのでソレゾレをElementシングルトンオブジェクト(コンパニオンなやつ)に組み込んでprivateにしてあげますよ

Element objectの定義デス

object Element {
  // 配列を基にElementを構築
  private class ArrayElement(val contents:Array[String]) extends Element
  // 文字列を基にElementを構築
  private class LineElement(s:String) extends Element{
    val contents = Array(s)
    override def width = s.length
    override def height = 1
  }
  // 特定範囲を指定文字で埋め尽くした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.make(height, line)
  }
  // ArrayElementの呼び出し用
  def elem(contents:Array[String]):Element = {
    new ArrayElement(contents)
  }
  // UniformElementの呼び出し用
  def elem(char:Char, width:Int, height:Int):Element = {
    new UniformElement(char, width, height)
  }
  // LineElementの呼び出し用
  def elem(line:String):Element = {
    new LineElement(line)
  }
}
そしてElementクラスへの組み込み

Elementクラスの各サブクラスをコンパニオンオブジェクトを利用して隠蔽できたので、隠蔽のための変更をElementクラスに適用しますよー

// コンパニオンオブジェクトをインポート
import Element.elem
// クラス定義
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 =  {
    // 隠蔽メソッドelemを適用
    elem(this.contents ++ that.contents)
  }
  // 並べメソッド
  def beside(that: Element):Element = {
    // 隠蔽メソッドelemを適用
    elem(
      for (
        (line1, line2) <- this.contents zip that.contents
      ) yield line1 + line2
    )
  } 
  // 表示するメソッド
  override def toString = contents.mkString("\n")
}

これによってクラス定義は抽象クラスのみになってしまったので、クライアントによるElementオブジェクトの利用はコンパニオンオブジェクト経由になるのかしらね?

実際の利用方法は後ろの節にのっているみたいなので、とらえずそこにたどり着いたら確認しますかねー

以上ー

とりあえずファクトリーメソッドの導入が済んだので次回はライブラリーの仕上げをしますよー