Scala Advent Calendar jp 2011: トレイトと自分型で簡単!コード分割

Scala Advent Calendar jp 2011の21日目の記事です。

最初に

Scala実践プログラミング』に記載されていたCakeパターンの解説を読んで自分型の威力を思い知り、自分でも簡単な例で実践してみました。

お題となる分割前のコードはこんな感じ。黒い四角がjkhlキー押下で上下左右に動くだけのswingアプリです。

import scala.swing._
import scala.swing.event.KeyTyped
import java.awt.Color

object Main extends SimpleSwingApplication{

  def top = new MainFrame{
    val panel = new Panel() {

      object Block{
        private var (px,py) = (0,0)
        def x = px
        def y = py
        def down(){ py-=1 }
        def up(){ py+=1 }
        def right(){ px+=1 }
        def left(){ px-=1 }
      }

      val blockSize = 10
      focusable = true
      peer.setPreferredSize(new Dimension(200, 200))

      override def paintComponent(g:Graphics2D){
	super.paintComponent(g)
	g.setColor(Color.BLACK)
	val (startx, starty) = (5,5)
	  g.fillRect((startx + Block.x) * blockSize, (starty - Block.y) * blockSize,
		     blockSize, blockSize)
      }

      listenTo(keys)
      reactions += {
	case KeyTyped(_,'h',_,_) => Block.left();repaint
	case KeyTyped(_,'j',_,_) => Block.down();repaint
	case KeyTyped(_,'k',_,_) => Block.up();repaint
	case KeyTyped(_,'l',_,_) => Block.right();repaint
      }
    }
    contents = panel
  }
}

Panelのインスタンスにほぼすべてのコードが入っていて、結合度が高い状態です。
このコードをトレイトと自分型を使って分離していきます。

Modelの分離

まずはobject Blockをtrait Modelに入れてobject Mainの外に出してやります。

trait Model{
  object Block{
    private var (px,py) = (0,0)
    def x = px
    def y = py
    def down(){ py-=1 }
    def up(){ py+=1 }
    def right(){ px+=1 }
    def left(){ px-=1 }
  }
}

PanelのインスタンスにMix-inしてやればコードの他の部分からもBlockオブジェクトは以前と同様に参照可能です。

val panel = new Panel() with Model {
  // Blockを参照するコード
}

Controllerの分離

次に入力の部分もトレイトでくくり出してobject Mainの外に持って行き、panelにMixinします。

trait Controller{
  listenTo(keys)
  reactions += {
    case KeyTyped(_,'h',_,_) => Block.left();repaint
    case KeyTyped(_,'j',_,_) => Block.down();repaint
    case KeyTyped(_,'k',_,_) => Block.up();repaint
    case KeyTyped(_,'l',_,_) => Block.right();repaint
  }
}

コンパイルしてみると、

[error] /Users/papamitra/src/scala/cake_tutorial/main.scala:20: not found: value listenTo
[error]   listenTo(keys)
[error]   ^
[error] /Users/papamitra/src/scala/cake_tutorial/main.scala:21: not found: value reactions
[error]   reactions += {
[error]   ^
[error] /Users/papamitra/src/scala/cake_tutorial/main.scala:21: reassignment to val
[error]   reactions += {
[error]             ^
[error] three errors found

おっと、コンパイルエラーです。Blockや、Panelのメンバであるreactions,listenToが参照できない為です。

そこで登場するのが自分型です。
自分型としてComponent(Panelの親クラス)とModelを指定すると、あたかも自身が指定したクラスであるかのように参照が解決されます。
そのため先ほどの参照エラーは回避されるようになります。

trait Controller{
  self: Component with Model =>  // 自分型の指定
  listenTo(keys)
  reactions += {
    case KeyTyped(_,'h',_,_) => Block.left();repaint
    case KeyTyped(_,'j',_,_) => Block.down();repaint
    case KeyTyped(_,'k',_,_) => Block.up();repaint
    case KeyTyped(_,'l',_,_) => Block.right();repaint
  }
}

これで入力部分も分離できました。後はModelのときと同様にPanelにMix-inしてやればOKです。

val panel = new Panel() with Model with Controller{
  // 省略
}

Viewの分離

続いて表示部分を分離します。blockSizeはPanelインスタンス生成時に決定できるよう抽象メンバとしました。
Controllerと同様に自分型としてComponentとModelを指定してやります。

trait View {
  self: Component with Model =>

  def blockSize:Int

  override def paintComponent(g:Graphics2D){
    super.paintComponent(g)
    g.setColor(Color.BLACK)
    val (startx, starty) = (5,5)
    g.fillRect((startx + Block.x) * blockSize, (starty - Block.y) * blockSize,
	       blockSize, blockSize)
  }
}

しかしこのコードはコンパイルエラーとなります。

[error] /Users/papamitra/src/scala/cake_tutorial/main.scala:35: value paintComponent is not a member of java.lang.Object with ScalaObject
[error]     super.paintComponent(g)
[error]           ^
[error] one error found

自分型のsuperを直接呼び出すことはできないのです。
ではどうすればよいか?実は以下のようにして回避が可能です。

trait ComponentTrait{ def paintComponent(g:Graphics2D)}

trait View extends ComponentTrait{
  self: Component with Model =>

  abstract override def paintComponent(g:Graphics2D){
    super.paintComponent(g)
    // 以下省略
  }
}

Componentが持っているpaintComponentと同じシグネチャのメソッドをもつトレイトを作り、Viewがそれを継承するようにします。Viewはそのメソッドをabstract overrideしています。
これでうまく機能する理由はコップ本に書いてあります。(第2版 p.223)

(前略)変わったこととは、abstract宣言されたメソッドでsuperをよびだしていることである。
通常のクラスでは、間違いなく実行時にエラーになるので、このような呼び出しは認められていない。
しかし、トレイトでは、このような呼び出しも成功するのである。
トレイト内でのsuper呼び出しは動的に束縛されるので(中略)、メソッドに対して具象定義を提供している他のトレイトないしはクラスの後で(afterメソッドとして)ミックスインされる限り正しく機能する。

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

つまり抽象メソッドを持つtrait(ここではComponentTrait)を継承してabstractをつけてオーバーライドしてやれば、具象メソッドを持つクラスを探しだしてsuper呼び出しを成功させるということらしい。
Scalaすごい!!

これでViewの分離も出来ました。最終的にobject Mainは以下のようにすっきりした形となりました。

object Main extends SimpleSwingApplication{
  def top = new MainFrame{
    val panel = new Panel() with Model with Controller with View{
      val blockSize = 10
      focusable = true
      peer.setPreferredSize(new Dimension(200, 200))
    }
    contents = panel
  }
}

まとめ

Scalaではトレイトと自分型を使うことで自由自在に感心事を分離できることがお分かりいただけたかと思います。

以下に完成したコードを上げておきます。
(repaintの位置が気に食わなかったので少し変更してあります)

import scala.swing._
import scala.swing.event.KeyTyped
import java.awt.Color

trait Model{
  self: View =>
  object Block{
    private var (px,py) = (0,0)
    def x = px
    def y = py
    def down(){ py-=1; reflect }
    def up(){ py+=1; reflect }
    def right(){ px+=1; reflect }
    def left(){ px-=1; reflect }
  }
}

trait View{
  def reflect
}

trait Controller{
  self: Component with Model =>
  listenTo(keys)
  reactions += {
    case KeyTyped(_,'h',_,_) => Block.left()
    case KeyTyped(_,'j',_,_) => Block.down()
    case KeyTyped(_,'k',_,_) => Block.up()
    case KeyTyped(_,'l',_,_) => Block.right()
  }
}

trait ComponentTrait{ def paintComponent(g:Graphics2D)}

trait ViewImpl extends ComponentTrait with View{
  self: Component with Model =>

  def blockSize:Int

  def reflect=repaint

  abstract override def paintComponent(g:Graphics2D){
    super.paintComponent(g)
    g.setColor(Color.BLACK)
    val (startx, starty) = (5,5)
    g.fillRect((startx + Block.x) * blockSize, (starty - Block.y) * blockSize,
	       blockSize, blockSize)
  }
}

object Main extends SimpleSwingApplication{
  def top = new MainFrame{
    val panel = new Panel() with Model with Controller with ViewImpl{
      val blockSize = 10
      focusable = true
      peer.setPreferredSize(new Dimension(200, 200))
    }
    contents = panel
  }
}