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


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

// ArrayElementをさらに拡張 
class LineElement(s:String) extends ArrayElement(Array(s)){
  override def width = s.length
  override def height = 1
}

今回はポリモーフィズムとかやっていきますよー

多相性と動的束縛

オブジェクト指向において継承やらなんやらでオブジェクトが様々な性質を持つようになる現象を多相性(ポリモーフィズム)というらしいんですが、Scalaでもだいたい同じような解釈っぽいですね。

とりあえず更なるポリモーフィズムのためにElementクラスのサブクラスを定義してやりますよー

UniformElementを定義

指定された幅と高さの範囲を、指定された文字で埋め尽くすElement形式をUniformElementとして定義してやります

class UniformElement(
  // 範囲を埋め尽くす文字を指定
  ch:Char,
  // 幅と高さを指定
  // スーパークラスではメソッドだったものをフィールドとしてオーバーライド
  override val width:Int,
  override val height:Int
  // Elementクラスを拡張
) extends Element {
  // 抽象メソッドcontentsを実装
  private val line = ch.toString * width
  def contents = Array.make(height, line)
}

これでElement一族が増加して様々なパターンが生まれたわけですな

ポリモーフィズム的な気分

Elementクラスの拡張的な発展でElement型は次のような多相性をもつわけです、を一覧してみます

// ArrayElement的な性質もあり
val e1:Element = new ArrayElement(Array("hoge", "huga"))
// LineElement的な性質もあり
val e2:Element = new LineElement("hello")
// UniformElement的な性質もあり
// Char型なので'(シングルクォート)で括りマス
val e3:Element = new UniformElement('x', 3, 4) 

// LineElementについてはこんな感じの代入も可能ですな
// …こっちが正当な経路だけども
val ae: ArrayElement = new LineElement("hello")
val e2:Element = ae

ついでに、こうやって並べて見ると右の型が左の型のサブ型であることも一目瞭然だぜ!だぞうです(´・ω・`)

動的束縛

コップ本的な動的束縛は次のようなことを指すみたいです

実際に呼び出されるメソッドの動作は、変数や式の方ではなく、実行時のオブジェクトのクラスによって決まる

ポリモーフィズム的に、拡張されたクラスの状態でメソッドの振る舞いは変わるよね?って解釈でいいかしら?

確認のためにとりあえずサンプルやってみますよー、まずは動的束縛的サンプルとして簡略化したElement一族を定義しますよ

// 大元のElementクラスですよ
abstract class Element {
  def demo() {println("Element's implementation invoked")}
}
// Elementクラスのサブクラスです
class ArrayElement extends Element {
  // メソッドdemoをオーバーライドしますよ
  override def demo() {println("Array Element's implementation invoked")}
}
// ArrayElementクラスのサブクラスですよ
class LineElement extends ArrayElement {
  // メソッドdemoを更にオーバーライドしますよ
  override def demo() {println("Line Element's implementation invoked")}
}
// Elementクラスのサブクラスですがメソッドのオーバーライドはしません
class UniformElement extends Element

それでは実際に上のクラス群のメソッドdemoを実行して動作を試してみますよー

// ArrayElementクラスのdemoを実行
scala> val e = new ArrayElement
e: ArrayElement = ArrayElement@123ce3f
scala> e.demo()
// ArrayElementバージョンのdemoが実行される
Array Element's implementation invoked

// LineElementクラスのdemoを実行
scala> val e = new LineElement
e: LineElement = LineElement@19d5fe6
scala> e.demo()
// LineElementバージョンのdemoが実行される
Line Element's implementation invoked

// UniformElementクラスのdemoを実行
scala> val e = new UniformElement
e: UniformElement = UniformElement@dd23cf
scala> e.demo()
// 大元のElementオリジナルのdemoが実行される
Element's implementation invoked

きちんと振る舞いが変わっていることが確認できますな(゚∀゚)

ファイナルメンバー宣言

クラス設計においてサブクラスにオーバーライドさせたくないことがよくありますが、そんな時はfinal宣言です!(`・ω・´)…って、うん力強く宣言したかっただけ(´・ω・`)

ちょっくらためしてみますよー

// スーパークラスです
class Cat {
   // final宣言してオーバーライドを禁止します
   final val seikaku = "猫っぽい" 
}
// サブクラスですよ
class Yashichi extends Cat{
  override val seikaku = "どちらかというと犬"
}

// finalしてんだからoverrideすんなってコンパイラ様に怒られました
<console>:6: error: error overriding value seikaku in class Cat of type java.lang.String("猫っぽい");
 value seikaku cannot override final member
         override val seikaku = "どちらかというと犬"
                      ^

まあ、上のクラスの例は現実にはfinal宣言されてないみたいなんですけどね(´・ω・`)ナマエヨブトトンデクル ネコ デス

クラスまるごとファイナル宣言

そもそもサブクラスを作らせたくなかったらクラス宣言にfinal修飾子を付ければいいそうです

これもサンプルやってみますよー

// final宣言クラスです
final class Hoge{
  val h = 3
}
// 無理やりサブクラスです
class HogeHoge extends Hoge

// final宣言してんだから継承すんなって怒られましたよ
<console>:5: error: illegal inheritance from final class Hoge
       class HogeHoge extends Hoge

うん、サブクラス作成禁止っていうメタファーが種の絶滅的な方向にしか思いつかなかったので無難な例にしてみたの(´・ω・`)

とりあえずfinal修飾子を使うことでクラスの構造を制御できそうですねー

合成か継承か

あるクラスを基に新しいクラスを作成する方法として合成と継承の2通りがあることを学びました、しかしできるだけ継承は使わずに合成を使うべきです…とあるのですが、合成と継承の具体的な違いがわからいです先生(´・ω・`)

…なので少しばかり整理してみますよ

合成と継承の違いについての考察的なもの

コップ本によればElement⇒ArrayElementの関係が合成で、ArrayElement⇒LineElementの関係が継承らしいです。

なので、クラスフィールドの型に他のクラス(ArrayElementだったらArrayクラス)を使っている関係であれば合成と呼んでいいのかも…ここにもそれっぽいこと書いてあるし…(´・ω・`)

どうもJava文化ではフィールドに別のクラスを取り込むことは特別なものとみなしているのかしらん?と勝手に解釈してみます…あとは使ってみて気づくしか無いのかも…

…そしていつものとおり教えてエロイ人。Let's 他力本願

継承をおすすめしない理由

継承ではスーパークラスを書き換えるとサブクラスが使えなくなるような「脆弱な基底クラス」と呼ばれる問題があるので、できれば合成を使うべき!だそうです(´・ω・`)

…でも、合成だと防げるのかしら?と思っても少し調べなおしてみたら、参考サイトによれば合成が云々というよりも、オーバーライド全開で構築するのではなくインターフェース(抽象クラス?)の実装をするように心がければ「脆弱な基底クラス」問題を防げるってことらしい(超意訳)…ってことでいいのかしら?

…ということは合成ってのはインターフェースの継承っていう解釈でいいのかしら?イマイチすっきりしないから、やっぱり教えてエロい人で(´・ω・`)

継承を使う基準

継承関係を持たせる場合はis-a関係をモデリングしているかを自問自答すべき、もしくはクライアントがスーパークラス型としてサブクラス型を使いたい場合だそうです、はい

ちなみに想定するクライアントがArrayElementをElementとして使うと想定できるので、Element⇒ArrayElementは継承関係を結ぶのが妥当…だそうで…

…なんだかとてもわからなくなってきたけども、とりあえず先に進みますよ(´;ω;`)

LineElementをElement&Arrayの合成関係にします

上記定義によればLineElementをArrayElementとして使うことは適切ではない(と、コップ本がおっしゃる)のでLineElementをElement直下のサブクラスとして定義すべき…だそうですよー、やりますよー(ややヤケクソで)

class lineElement(s:String) extends Element {
  // フィールドにArray型を組み込んだので合成です(よね?)
  val contents = Array(s)
  override def width = s.length
  override def height = 1
}

前の定義だとArrayElementのcontentsをそのまま継承していたLineElementですが、上のような定義で自分自身でcontentsフィールドにArray型に組み込んでおりますので合成関係になってるそうです(´・ω・`)

以上ー

…後半の合成云々で混乱気味なので、先をやっていくうちに整理が出来ればいいなぁ…と

とりあえず今回までの内容は次のとおりですよー

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

とりあえずこんな感じですなー、次回はElement一族のメンバーをガシガシ実装していきますよ