ScalaでDI (Cake Pattern 導入編)


こんにちは、馬場です。

唐突ですが、現在Play! + Scalaで開発しています。
それまではほとんどJava (たまにRuby/Rails)で作っていたので、本格的にサービスをScalaで作るのは初めて。
慣れないながらもすごく楽しくやっていましたが、プロジェクト開始早々でてきた不満。それは

「DIしたい…」

なぜなら「テストが面倒だから」。
データベースアクセス、webapiの呼び出し、メール送信と外部リソースへアクセスする処理のオンパレード。テストのたびに実行していたらスローテストに陥ることは目に見えています。リファクタリング/パフォーマンスチューニング/ライブラリのアップデートに耐えるためにも、ここはぜひともDIを導入しておきたい。

というわけで、Scala アプリケーションへのDIの導入体験を紹介したいと思います。

Java で DI

まずはJavaのDIコンテナSpringの実装例をみていきましょう。
例えば、Web APIを呼び出すようなプログラムでは、実際にWeb APIを呼び出す処理を切り出し、インスタンス生成時に設定するようにします。

public class ProfileService{

  private TwitterApi twitterApi;

  public ProfileService(TwitterApi twitterApi){
    this.twitterApi = twitterApi;
  }

  public int getProfileCharCount(String twitterId){
    String profile = twitterApi.getProfile(twitterId);
    return profile.length;
  }

}

TwitterApi は「取り替え可能」になるようにインタフェースにします。

public interface TwitterApi{

  public String getProfile(String twitterId);

}

本番で利用する、実際にWeb APIの呼び出しを行うクラスを作成します。このクラスはインタフェースを実装します。

public class TwitterApiImpl implements TwitterApi{

  public String getProfile(String twitterId){
  /// APIを呼び出す複雑な処理のあれやこれや
  }

}

TwitterApiImplを生成したり、ProfileServiceにそれをsetするのはDIコンテナのSpringの役割です。
以下のように、どのようなインスタンスをProfileServiceに設定するのか、XMLに記述します。

<beans>
<bean id="twitterApi" class="TwitterApiImpl"/>
<bean id ="profileService" >
<constructor-arg ref="twitterApi" />
</bean>
</beans>

コード中では、

ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
ProfileService profileService = (ProfileService)ctx.getBean("profileService");

のようにProfileServiceを取得します。こうしておけばWeb APIの認証などの情報が変わった場合でも、ProfileServiceのコードを変更せずに、
設定ファイルだけを編集すれば、変更されたWeb API呼び出しクラスをProfileServiceで利用する事ができます。

さらにUnitTest では、簡単にtwitterApiをモックに差し替えることができます。

public class ProfileServiceTest{

  @Test
  public void test_プロファイルを取得する(){
    TwitterApi twitterApi = Mockito.mock(TwitterApi.class);
    Mockito.when(twitterApi.getProfile("AyakoBaba")).thenReturn("馬場彩子のプロフィール");

    int result = new ProfileService(twitterApi).getProfile("AyakoBaba");
    assertEquals(11,result);
  }

}

Mockitoはモックを作成するためのJava ライブラリです。

Web APIを呼び出す処理は設定などが複雑になりますし、公開サービスなら特に実行のタイミングごとに結果が異なったりしがちです。
ここで確認したいのは「APIから取得した文字の数を返しているか」というところで、API呼び出しの部分ではありません。
テストしたい処理にフォーカスすれば、API呼び出しはモックで十分。また実際に外部のWeb APIを呼び出すのとは違い、テストも瞬殺で終わります。

Scala で DI - CakePattern

Javaの開発で何度も助けられたDI。どうにかこのプロジェクトにもDIを導入したいと考えましたが、DI嫌いのPlay! frameworkにはそのような仕組みはありません。Scala といえどもJVM言語なので、Spring / Guice などのJava のDIフレームワークも利用できます。ですが、ここはせっかく(?)なのでScalaらしい方法で実装したい。ということで、バクの本にもちらっと書いてある「Cake Pattern」をやってみることにしました。
Cake Pattern は、Scala のデザインパターンで、構造がケーキのように水平に何段にも重ねたようにも、垂直にきりだしたようにもみえるのでそのように名付けられたのですが、自分でいっていてもよくわからないので、とにかくコードをみてみましょう!

まず、場面ごと(本番とテスト)で切り替えたいWeb APIの呼び出し処理は、traitで宣言します。

trait TwitterApi {

  def getProfile(twitterApi:String):String

}

その後すぽっとComponent traitで取り囲みます。TwitterApiはTwitterApiComponentが提供します。

trait TwitterApiComponent{
  val twitterApi:TwitterApi

  trait TwitterApi {
    def getProfile(twitterApi:String):String
  }

}

次にProfileServiceを作成します。ProfileServiceはTwitterApiを利用したいので、このクラスをすぽっとComponent traitで取り囲み、さらにTwitterApiComponent を自己参照します。これにより内部のProfileServiceクラスでは、TwitterApiが提供するtwitterApiが利用できます。

trait ProfileServiceComponent{
  this:TwitterApiComponent =>

  class ProfileService {
    def count(twitterId:String) = twitterApi.getProfile(twitterId).length
  }

}

本番で利用する、実際にWeb APIの呼び出しを行うクラスを作成します。TwitterApiを継承するTwitterApiImplクラスを作成し、これもTwitterApiComponentを継承したComponent traitですぽっと囲みます。

trait TwitterApiComponentImpl extends TwitterApiComponent {

  class TwitterApiImpl extends TwitterApi{
    def getProfile(twitterApi:String) = {
    //複雑なあれやこれや
    }
  }

}

さて、最後にコンテナにあたるobjectを作成します(objectにするのはsingletonにしたいから)。このComponentRegistryは、ProfileServiceComponent および TwitterApiComponentImpl を継承しています。

object ComponentRegistry extends ProfileServiceComponent with TwitterApiComponentImpl{

  val twitterApi = new TwitterApiImpl()

  val profileService = new ProfileService()

}

コード中では、

val profileService = ComponentRegistry.profileService

のように、ComponentRegistryからprofileServiceを取得します。profileService内部で利用するtwitterApiはComponentRegistry で生成しているTwitterApiImplになります。
テストでは以下のようにテストクラスでProfileServiceComponentとTwitterServiceComponentをwithします。twitterApiをモックに置き換えてテストを実施することもできます。

class ProfileServiceTest extends UniFunSuite with ShouldMatchers with ProfileServiceComponent with TwitterServiceComponent{

  val twitterApi = Mockito.mock(classOf[TwitterApi])

  test("プロファイルの文字数を数える"){
    Mockito.when(twitterApi.getProfile("AyakoBaba")).thenReturn("馬場彩子のプロフィール")

    val result = new ProfileService(twitterApi).getProfile("AyakoBaba")
    result should be (11)

  }

}

Cake Pattern ってどうなんだろう。

なんとかCake PatternでDIを実装することができました。DIがなかったころよりテストが早いので、私比3倍快適な開発ライフ。Play! Frameworkの人とは意見が違いますが、ある程度の規模と複雑さがあるアプリケーションの開発にはDIはなくてはならないもの、という感じました。

それでは、Cake Patternはどうだったんだろう、と振り返ると…
DIコンテナの設定が、SpringのようにXMLではなく通常のScalaのプログラムである、という点はよかったです。設定にミスがあればコンパイル時に発見できますし、ただのプログラムなのでインスタンスの生成方法を柔軟に記述することができます。Springは高機能で自動バインディングなどもあり楽なのですが、テンプレートと少しちがったことをやろうとするとどうしたらよいかわからない複雑さがありますし、XMLの設定ファイルを編集するのも気が重いです。maven と sbt の印象の違いに似ている、というか。
ただ当然、いくつか運用上気になることはあります。まず、開発が進むにつれ関係しあうクラスがどんどん増えていき、気づけばComponentRegistry で Component trait の with が祭りになってしまったこと。何段ケーキなんだよ、という感じです。途中で、ComponentRegistry自体をいくつかのtraitに分割しましたが、規模がもう少し大きくなると改めて課題になるかもしれません。もうひとつは、ちょっとわかりにくいというところです。この開発プロジェクトでは、途中から人がちょっとずつ増えていったのですが、Cake Patternは開発でつまづく箇所堂々第2位だったと思います(1位はまた機会があればお話します) 。一見して、Component traitですぼっと取り囲む構造にしている意図がわからないんですよね。これは人が増えるごとにCake Patternについて改めて説明しなかったからかもしれませんが。次来た人にはこのBlogを読んでもらうことにします。

次回予告

今回はCake Patternについてみてきましたが、ScalaのDIのパターンは他にもまだ(少しだけ)あります。
次回は、TwitterがEffective Scalaで紹介しているパターン、および Lift フレームワークでのDIの実装についてみていきたいと思います。


This entry was posted in 技術. Bookmark the permalink.