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メソッドとして)ミックスインされる限り正しく機能する。
- 作者: Martin Odersky,Lex Spoon,Bill Venners,羽生田栄一,水島宏太,長尾高弘
- 出版社/メーカー: インプレス
- 発売日: 2011/09/27
- メディア: 単行本(ソフトカバー)
- 購入: 12人 クリック: 235回
- この商品を含むブログ (46件) を見る
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 } }