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 を実行

Cake Pattern の簡単な例を書いておく

Cake Pattern の説明は Programming Scala よりも、http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di/ の方が分かりやすかったです。

一応、自分なりに理解できたつもりでいるので、とりあえず簡単な例を書いておきます。
出現するコンポーネントは Foo、Bar の二つ。Bar が Foo に依存している例です。

package sample01

trait FooComponent {
  val foo: Foo
  class Foo {
    def output(text: String) =
      println(text)
  }
}

trait BarComponent { self: FooComponent =>
  val bar: Bar
  class Bar {
    def process(name: String) =
      foo.output("Hello %s world!".format(name))
  }
}

object ComponentRegistry extends FooComponent with BarComponent {
  val foo = new Foo
  val bar = new Bar
}

object Main {
  def main(args: Array[String]) =
    ComponentRegistry.bar.process("Scala")
}

実行すると "Hello Scala world!" と出力されます。
この例の場合、コンポーネント提供側と利用側とが同じtraitを使用しています。このため、いまいち理解しづらいかもなので、trait を分割してみます。*1

package sample02

// コンポーネント利用者に公開
trait Foo {
  def output(text: String): Unit
}
trait FooInstance {
  val foo: Foo
}

trait Bar {
  def process(name: String): Unit
}
trait BarInstance {
  val bar: Bar
}

// コンポーネントの実装  
trait FooComponent {
  class FooImpl extends Foo {
    def output(text: String) =
      println(text)
  }
}

trait BarComponent { self: FooInstance =>
  class BarImpl extends Bar {
    def process(name: String) =
      foo.output("Hello %s world!".format(name))
  }
}

// コンポーネントリポジトリ
object ComponentRegistry extends FooInstance with FooComponent
                            with BarInstance with BarComponent {
  val foo = new FooImpl
  val bar = new BarImpl
}

object Main {
  def main(args: Array[String]) =
    ComponentRegistry.bar.process("Scala")
}

この例では、Bar コンポーネント(の実装クラスである BarImpl)は FooInstance トレイトのフィールド val foo: Foo を参照して、Foo コンポーネントを利用しています。このとき、Bar コンポーネントは Foo コンポーネントの実装に依存することなく、また foo には有効な値が設定されていることを前提として利用できています。これは、DI コンテナ利用時に、依存コンポーネントをフィールドとして参照するのと似ているかと思います。
また、DI コンテナ利用時は、コンテナが依存コンポーネントをフィールドに設定してくれますが、Cake Pattern の場合はすべてのコンポーネントを保持する ComponentRegistry で、各フィールドにコンポーネントを設定します。ちなみに、フィールドは val として宣言されているため、設定漏れがある場合はコンパイルエラーとなります。実行せずとも設定漏れに気付けるというのは嬉しい点です。


Cake Pattern を利用した場合、DI コンテナが提供する機能のいくつかを言語の機能で実現することが出来ます。ただ、AOP であったりスコープの制御(リクエストのたびに生成等)であったりは DI コンテナの方が扱いやすいのかなと思います。うまく DI コンテナと Cake Pattern を併用できるとより良いのかも知れません。

追記(2010/01/19)

インタフェースと実装を分離するのは、http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di/ の一番下に書いてあったです。よく読んでいませんでした(汗

*1:複数の実装があるわけでもないので、Cake Pattern を実際に使用する時には trait を分割する必要は無いと思います。

親トレイトで定義されている val を、子トレイトで override するとインスタンス化に失敗する

子側のトレイトで親の val を override すると、コンパイルは成功するにも関わらず、実行時に ClassFormatError が出ます。
以下は現象を再現させるコード。

trait Base {
  val value: String = "base"
}
  
trait Child extends Base {
  override val value: String = "child"
}
  
object OverrideTraitSample {
  def main(args: Array[String]) {
    val target = new AnyRef with Child
    println(target.value)
  }
}

これをコンパイル&実行した結果は以下。


C:\development\workspaces\sandbox\scala-sandbox\src>scalac OverrideTraitSample.scala

C:\development\workspaces\sandbox\scala-sandbox\src>scala OverrideTraitSample
java.lang.ClassFormatError: Duplicate method name&signature in class file OverrideTraitSample$$anon$1
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:260)
at java.net.URLClassLoader.access$000(URLClassLoader.java:56)
at java.net.URLClassLoader$1.run(URLClassLoader.java:195)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at java.lang.ClassLoader.loadClass(ClassLoader.java:303)
at java.lang.ClassLoader.loadClass(ClassLoader.java:248)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:316)
at OverrideTraitSample$.main(OverrideTraitSample.scala:11)
at OverrideTraitSample.main(OverrideTraitSample.scala)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at scala.tools.nsc.ObjectRunner$$anonfun$run$1.apply(ObjectRunner.scala:75)
at scala.tools.nsc.ObjectRunner$.withContextClassLoader(ObjectRunner.scala:49)
at scala.tools.nsc.ObjectRunner$.run(ObjectRunner.scala:74)
at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:154)
at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)

C:\development\workspaces\sandbox\scala-sandbox\src>

コンパイラのバグなのか何なのか・・・
ちなみに使用した scala のバージョンは 2.7.7 final です。

trait ではなく、無名クラスで override してみた場合、ClassFormatError は出ないですが、そもそも override 出来ていません :-P
無名クラスで override した例は以下。

// 〜trait Base は省略〜
object OverrideTraitSample {
  def main(args: Array[String]) {
    //val target = new AnyRef with Child
    val target = new Base { override val value = "anon" }
    println(target.value)
  }
}

コンパイル&実行結果は以下。


C:\development\workspaces\sandbox\scala-sandbox\src>scalac OverrideTraitSample.scala

C:\development\workspaces\sandbox\scala-sandbox\src>scala OverrideTraitSample
base

C:\development\workspaces\sandbox\scala-sandbox\src>

val を override 出来ていないため、"anon" ではなく、"base" が出力されている。
そもそも val は override できないのか・・・?

追記(2010/01/13)

ちょろっと仕様書の中をさがしてみましたが、trait の val は override 出来ないといった記述は発見できず。
ワークアラウンドですが、val ではなく def で定義しておけば期待どおりの結果になります。

trait Base {
  //val value: String = "base"
  def value: String = "base"
}
  
trait Child extends Base {
  //override val value: String = "child"
  override def value: String = "child"
}
  
object OverrideTraitSample {
  def main(args: Array[String]) {
    val target = new AnyRef with Child
    println(target.value)
  }
}


コンパイル&実行。


C:\development\workspaces\sandbox\scala-sandbox\src>scalac OverrideTraitSample.scala

C:\development\workspaces\sandbox\scala-sandbox\src>scala OverrideTraitSample
child

C:\development\workspaces\sandbox\scala-sandbox\src>


ちなみに Base と Child の両方を class にしても期待通り動きます。trait の場合だけ期待通りに動かないみたいですね。

SiteMap も JRebel(JavaRebel) で HotDeploy されるようにする

Liftを使用しての開発では mvn scala:cc と mvn:jetty:run をしてファイル保存のタイミングで変更が反映されるようにしていました。
scala ファイルを変更すると scala:cc により自動でコンパイルが実行され、class ファイルが変更されたことにより Jetty がクラスをリロードするという流れです。

ただ、ときどき Jetty がリロードしてくれなかったりと、ちょっと動作がいまいちな時があったので、JRebel を使用して HotDeploy する方法に変えました。
JRebel の設定などは JavaRebelとscala:ccで快適Lift開発 - スコトプリゴニエフスク通信 を参考にしました。
また、JRebel は scala を応援しているらしく、scala から生成したクラスファイルに限り無償でライセンスを得ることができます。scala 開発者用ライセンスは http://www.zeroturnaround.com/scala-license/ の各項目に必要事項を入力後、メールの添付ファイルで送られてきます*1。ライセンスファイルは JRebel の jar ファイルと同じディレクトリに配置しておけば OK です。

さて、さくさく HotDeploy がきいてくれて、IDE ようのプラグインもあったりとなかなか良い JRebel なのですが、Lift で使用した場合に SiteMap の設定変更が反映されません。これは JRebel が問題というよりも、SiteMap を設定している Boot.boot メソッドは起動時にしか呼び出されていないためのようです。
そこで、リクエスト毎に SiteMap を再設定するようにして、SiteMap の設定変更も HotDeploy されるようにしました。

まずは、SiteMap の設定部分を Boot から別クラスに移動します。

package bootstrap.liftweb

import _root_.net.liftweb.util._
import _root_.net.liftweb.http._
import _root_.net.liftweb.sitemap._
import _root_.net.liftweb.sitemap.Loc._
import Helpers._

/**
  * A class that's instantiated early and run.  It allows the application
  * to modify lift's environment
  */
class Boot {
  def boot {
    // where to search snippet
    LiftRules.addToPackages("sandbox")

    // Build SiteMap
    LiftRules.setSiteMap((new SiteMapHolder).siteMap) // (1)
  }
}

class SiteMapHolder { // (2)
  val siteMap = SiteMap(Menu(Loc("Home", List("index"), "Home")))
}

(1) の部分にあった SiteMap の設定を (2) の SiteMapHolder クラスに移動しています。SiteMapHolder は class にしてあるのですが、これは object にすると JRebel がうまく HotDeploy してくれないような動きをしたためです。

つぎに SiteMap を再設定するサーブレットフィルタを作成します。

package bootstrap.liftweb

import _root_.javax.servlet.{Filter, FilterChain, FilterConfig, ServletRequest, ServletResponse}
import _root_.net.liftweb.http.LiftRules

class ReloadSiteMapFilter extends Filter {
  def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
    LiftRules.setSiteMap((new SiteMapHolder).siteMap) // (3)
    chain.doFilter(req, res)
  }

  def init(config: FilterConfig): Unit = ()

  def destroy(): Unit = ()
}

(3) の部分で LiftRules に SiteMap を再設定しています。おそらくこのタイミングで JRebel の HotDeploy がきき、変更後の SiteMapHolder が使用されます。

最後に web.xml を修正して、サーブレットフィルタを追加します。

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
	  PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
	  "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  <filter>
    <filter-name>ReloadSiteMapFilter</filter-name>
    <filter-class>bootstrap.liftweb.ReloadSiteMapFilter</filter-class>
  </filter>
  
  <filter>
    <filter-name>LiftFilter</filter-name>
    <display-name>Lift Filter</display-name>
    <description>The Filter that intercepts lift calls</description>
    <filter-class>net.liftweb.http.LiftFilter</filter-class>
  </filter>
  
  <filter-mapping>
    <filter-name>ReloadSiteMapFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  
  <filter-mapping>
    <filter-name>LiftFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>

あとは JRebel を有効にして、Jetty を起動。


c:\develop\workspaces\lift-sandbox>set MAVEN_OPTS=-noverify -javaagent:C:\development\libraries\JRebel.jar %MAVEN_OPTS%

c:\develop\workspaces\lift-sandbox>mvn jetty:run

ついでに、scala ファイル変更時にコンパイルが実行されるように、scala:cc を別のコマンドプロンプトで実行。

c:\develop\workspaces\lift-sandbox>mvn scala:cc

これで、SiteMap に Menu を追加したり、Loc.Hidden 等の設定を追加した場合でも、HotDeploy が効くようになります。ヨカッタヨカッタ。

*1:ちなみにこのフォーム、入力に不備がなく、送信に成功した場合でも同じ画面に遷移するため、送信できたのかいまいち分かりづらかったりします(汗

main メソッドが見つからないといわれたら・・・

先日、Scala プログラムを書いていたときのことです。object に main メソッドを定義しているにもかかわらず、main メソッドが見つからないといわれました。
以下のようなプログラムです。

import java.awt.Dimension
import javax.swing.JFrame

object Sample {
  def main(args: Array[String]): Unit = {
    val sample = new Sample
    sample.setVisible(true)
    sample.pack()
  }
}

class Sample extends JFrame {
  setPreferredSize(new Dimension(320, 240))
  setTitle("Sample")
}

上記をコンパイル&実行すると・・・


c:\development\workspaces\scala-sandbox\misc>scalac Sample.scala

c:\development\workspaces\scala-sandbox\misc>scala Sample
java.lang.NoSuchMethodException: Sample.main([Ljava.lang.String;)

というように、main メソッドが見つからないという状態になります。

コンパニオンオブジェクトは、バイトコード上では $ の付加されたクラス名となるようです。このため、Sample.main ではコンパニオンクラスの main メソッドを参照しようとして、見つからないという状態になっています。


では、$ をつければ実行できるのかと思い試しました。


c:\development\workspaces\scala-sandbox\misc>scala Sample$
java.lang.NoSuchMethodException: Sample$.main is not static
今度は not static といわれてしまい、やはり実行できませんでした。

コンパニオンオブジェクトのメソッドは static ではないようです。javap してみると以下のとおり、たしかに static ではなかったです。

Compiled from "Sample.scala"
public final class Sample$ extends java.lang.Object implements scala.ScalaObject{
    public static final Sample$ MODULE$;
    public static {};
    public Sample$();
    public void main(java.lang.String[]);
    public int $tag()       throws java.rmi.RemoteException;
}


一方、object が対になる class を持たない場合は、$ ありも $ なしも、両方ともに object のメソッドが定義されていました。
class Sample を class SampleFrame に変更して、コンパイル&javap してみました。

== javap Sample$ の結果 ==
Compiled from "Sample.scala"
public final class Sample$ extends java.lang.Object implements scala.ScalaObject{
    public static final Sample$ MODULE$;
    public static {};
    public Sample$();
    public void main(java.lang.String[]);
    public int $tag()       throws java.rmi.RemoteException;
}
== javap Sample の結果 ==
Compiled from "Sample.scala"
public final class Sample extends java.lang.Object{
    public static final void main(java.lang.String[]);
    public static final int $tag()       throws java.rmi.RemoteException;
}

Sample のほうには static なメソッドが、Sample$ の方には static ではない同名のメソッドが入っていました。
コップ本にもこのあたりのことが書いてあったような気がするのですが、今手元にないので今度確認します。

教訓: main メソッドを含む object はコンパニオンにしてはいけない。

JavaでARを試してみる

冬休み中にARをやってみようと思い*1、とりあえずサンプルを動かしてみました。

Java で使える AR 用のライブラリを探したところ、ARToolkitJava に移植した NyARToolkit というものがあったので、これを試すことにしました。

NyARToolkit は http://nyatla.jp/nyartoolkit/wiki/index.php から入手しました。「ダウンロード」のリンク先から NyARTookkit for Java の zip ファイル(NyARToolkit-2.4.2.zip)をダウンロードしました。

続いて、ダウンロードした zip ファイルに含まれる readme.ja.txt に従いいくつかのソースフォルダを eclipse のプロジェクトにインポートします。とりあえず JMF + Java3D のサンプルをためそうと思い、以下のフォルダをインポートしました。

そうするとコンパイルエラーがでるので、足りていない JMF と Java3D の jar をプロジェクトに追加します。

JMF は Java で音声やカメラなどを操作するときに使用するAPI です。http://java.sun.com/javase/technologies/desktop/media/jmf/ から入手できます。JMF をインストール後、c:\Program Files\JMF2.1.1e\lib にある jmf.jar をプロジェクトに追加しました。

Java3DJava で 3D オブジェクトをレンダリングするのに使用する API です。http://java.sun.com/javase/technologies/desktop/java3d/ から入手できます*2。こちらもインストール後に c:\Program Files\Java\Java3D\1.5.1\lib\ext にあるすべての jar ファイル - j3dcore.jar, j3dutils.jar, vecmath.jar - をプロジェクトに追加しました。

この時点でコンパイルエラーが無くなったので、試しに jp.nyatla.nyartoolkit.java3d.sample.NyARJava3D を実行してみましたが、データファイルが見つからないらしく例外が発生。なので、Data フォルダもプロジェクト直下にコピーし、jp.nyatla.nyartoolkit.java3d.sample.NyARJava3D 実行時のワークディレクトリが sample/java3d になるように、実行の設定を変更*3

再度サンプルを実行すると、カメラから取り込まれた映像の表示されたウィンドウが開きました。
立方体らしき3Dの物体も描画されています・・・が位置や大きさが一定していません。まだ、マーカーを用意していないので、少しでもマーカーに似ている模様のところに表示されているのかも知れないです。

マーカーは Data フォルダ内にある pattHiro.pdf を使用すればよいようです。なので、印刷してカメラの前にかざしてみました。

ちゃんとマーカーの上に立方体が表示されました。マーカーの向きを変えると、まるでマーカーの紙に張り付いているかのように、立方体も向きを変えました。カメラからの画像自体は二次元であるにもかかわらず、マーカのサイズや辺の向きなどによって、ちゃんと3次元空間にあるかのように振舞ってくれるのが面白いです。

サンプルのソースコード自体は非常に短く、何をやっているのかもわかりやすいです。ライブラリの便利さが実感できます。
明日以降はJava3Dの勉強になるかも・・・自分で何か作るには、3Dの知識が必須っぽいので。

*1:弊社2年目の子が急成長していて、まけてらんないと思いまして・・・

*2:Downloads にある「Java3D 1.5.1 API」を開き、「Download Java 3D 1.5.1 Software」から入手

*3:メニューバーの[Run]→[Run Configurations...]、設定ダイアログで NyARJava3D の Arguments タブを表示し、Working directory の設定を変更する

The Cake Pattern による DI

前に、Java の DI コンテナと Scala の組み合わせはどうにもいまいちと Twitter でつぶやいていたら、とある人から Programming Scala に DI のサンプルがあるよ!っておしえてもらいました。

以下の URL は Programming Scala の DI についての箇所です。*1
http://programming-scala.labs.oreilly.com/ch13.html#DependencyInjectionInScala

以下、私が理解した内容を忘れないようにメモっておきます。
読んでみると、DI コンテナのかわりに Cake パターンなる方法で、DI が対象とする問題を解決出来る模様です。*2


例として出ている Twitter クライアントのコンポーネントをクラス図にしてみました。

コンポーネントの役割は以下のとおり。

  • TwitterClientUIComponent - UI
  • TwitterLocalCacheComponent - ローカルキャッシュ
  • TwitterServiceComponent - Twitter サービスにアクセスする
  • TwitterClientComponent - クライアント

これらのコンポーネントはすべて同じ構造をとっています。

  • コンポーネントの振る舞いをあらわす、ネストしたトレイトやクラスを定義している
  • ネストしたトレイトやクラスの型をもつ val を宣言している

この時点での各コンポーネント間の依存は以下のとおり。

  • TwitterClientUI → Tweeter
  • TwitterClient → TwitterClientUI, TwitterLocalCache, TwitterService

TwitterClientUI と TwitterClient 間で依存関係が循環しているような気がしないでもないですが、TwitterClient → TwitterClientUI は実行時に参照しているだけなので多分大丈夫。

ここまできたら、すべての 〜Component トレイトを継承したクラスを作成して、各コンポーネントの実装を設定すれば良いはずです。この 〜Component トレイトを継承したクラスが、DI コンテナでの設定ファイルに対応するのだと思います。
例では TextClient というクラスで、すべての 〜Component トレイトを継承して、各コンポーネントの実装を設定しています。


以下は、試しに例をコーディングして気になったことです。

  • TextClient クラスでは実装を選択するだけにしたい。例のように各コンポーネントの実装自体をしていると、各コンポーネントに対する単体テストがやりづらいように思える。
  • DI コンテナで設定ファイルにあたるクラスで大量のトレイトを継承する必要が出てくる。設定ファイルを分割するように、うまいことクラスを分割できないだろうか?


とりあえず、私の理解した Cake パターンの使い方は以下のとおり・・・かな。

  • コンポーネントごとにトレイトを作成する
    • trait FooComponent とか
  • コンポーネントの振る舞いはネストしたトレイトor抽象クラスとして定義する
    • trait FooComponent { trait Foo } とか
  • 振る舞いのインスタンスを保持する抽象valを、コンポーネントのトレイトに宣言する
    • trait FooComponent { val foo: Foo; trait Foo } とか
  • 利用側コンポーネントは、利用するコンポーネントのトレイトを self-type annotation で指定する
    • trait BarComponent { self: FooComponent =>; val bar: Bar; trait Bar }
  • 必要なコンポーネントのトレイトを継承した、コンテナ代わりのクラスを作成し実装を設定する
    • class Baz extends FooComponent with BarComponent { val foo = new Foo {...}; val bar = new Bar {...} }
  • コンテナ代わりのクラスをインスタンス化し、コンポーネントを利用する


実際に使えば、もう少しいろいろ見えてくるかもしれませんが、まだなんともしっくりきません・・・(´・ω・`)

追記

興味のある方は、単純な例で Cake Pattern を試した Cake Pattern の簡単な例を書いておく も参考にどうぞ

*1:Programming Scala はネット上で読めたりします。ありがたい。http://programming-scala.labs.oreilly.com/

*2:"the Cake Pattern, which can replace or complement these other dependency injection mechanisms." なので、置き換える、または補完するってことですかね・・・