Chain of Responsibility っぽいことを trait を利用して実装してみる

Chain of Responsibility というよりも、http://itpro.nikkeibp.co.jp/article/COLUMN/20081009/316549/?P=2 の方法を変形しただけなんですけどね。

まず、リクエストを処理するメソッドを持つ、ハンドラの trait をつくります。

trait Handler {
  def handleRequest(request: Symbol): String = error("Unknown request.")
}

次に、特定のリクエストを処理するハンドラの実装をつくります。以下の例は 'foo と 'bar を処理するハンドラになります。

// 'foo を処理して "Hello Foo!!" を返す
trait FooHandler extends Handler {
  override def handleRequest(request: Symbol): String = request match {
    case 'foo => "Hello Foo!!"
    case other => super.handleRequest(other)
  }
}

// 'bar を処理して "Hello Bar!!" を返す
trait BarHandler extends Handler {
  override def handleRequest(request: Symbol): String = request match {
    case 'bar => "Hello Bar!!"
    case other => super.handleRequest(other)
  }
}

自身で処理でき無いリクエストの場合は super.handleRequest を呼び出して、次のハンドラに処理を委譲します。

あとはハンドラを連結(=with)した状態でインスタンスを生成します。コンパイル時にハンドラの実装を指定するため、動的にハンドラを追加/削除することは出来ないですが、多くの場合はこれで困らないかと思います。

// MEMO: new FooHandler with BarHandler で問題ないのだけど、
//       気分的に new Handler with … としています
val handler: Handler = new Handler with FooHandler with BarHandler

実際に ScalaTest をつかって実行してみました。

import org.scalatest.WordSpec
import org.scalatest.matchers.ShouldMatchers

class HandlerTest extends WordSpec with ShouldMatchers {
  "A handler" should {
    val target: Handler = new Handler with FooHandler with BarHandler
    "return 'Hello Foo!!' message when 'foo request is handled" in {
      target.handleRequest('foo) should be === ("Hello Foo!!")
    }
    "return 'Hello Bar!!' message when 'bar request is handled" in {
      target.handleRequest('bar) should be === ("Hello Bar!!")
    }
    "throw RuntimeException when an unknown request is handled" in {
      evaluating { target.handleRequest('unknown) } should produce [RuntimeException]
    }
  }
}

以下はIntelliJ 上での実行結果です。*1


Testing started at 11:53 ...
Run starting. Expected test count is: 3
HandlerTest:
A handler
- should return 'Hello Foo!!' message when 'foo request is handled
- should return 'Hello Bar!!' message when 'bar request is handled
- should throw RuntimeException when an unknown request is handled
Run completed in 627 milliseconds.
Total number of tests run: 3
Suites: completed 1, aborted 0
Tests: succeeded 3, failed 0, ignored 0, pending 0
All tests passed.

補記

Hanlder トレイトには handleRequest のデフォルト実装を持たせないで、別途 BaseHandler クラスみたいなものを作ったほうが役割が明確になっていいかもしれません。

trait Hanlder {
  def handleRequest(request: Symbol): String
}
class BaseHandler extends Handler {
  def handleRequest(request: Symbol): String = error("Unknown request.")
}

この場合、Handler の handleRequest は実装を持っていないため、FooHanlder と BarHandler の handleRequest を abstract override として定義する必要があります。あとは、上に書いてあるのと同じように、with してインスタンス化するだけです。

val handler: Hanlder = new BaseHandler with FooHandler with BarHandler

別の方法

ハンドラを PartialFunction として作成して orElse で合成するという方法でも、手軽に類似のことができます。この方法であれば実行時にハンドラを追加/削除することも可能かと思います。

import org.scalatest.WordSpec
import org.scalatest.matchers.ShouldMatchers

class HandlerTest extends WordSpec with ShouldMatchers {
  val fooHandler: PartialFunction[Symbol, String] = {
    case 'foo => "Hello Foo!!"
  }
  val barHandler: PartialFunction[Symbol, String] = {
    case 'bar => "Hello Bar!!"
  }
  
  "A handler" should {
    val target = fooHandler orElse barHandler
    "return 'Hello Foo!!' message when 'foo request is handled" in {
      target('foo) should be === ("Hello Foo!!")
    }
    "return 'Hello Bar!!' message when 'bar request is handled" in {
      target('bar) should be === ("Hello Bar!!")
    }
    "throw RuntimeException when an unknown request is handled" in {
      evaluating { target('unknown) } should produce [RuntimeException]
    }
  }
}

*1:-o オプションをつけて ScalaTest を実行