2011年12月23日金曜日

ScalazでBean Validation

このエントリはScala Advent Calender 2011の第23日目です。

Java業界で今年最大のニュースといえば、Oracle Open Worldで発表されたOracleのクラウド参入を上げる人も多いでしょう。OracleのPaaSが成功するのかどうかは未知数ですが、Java EEがPaaSのプラットフォームとして強力に推進されることは確実で、Java VM系での標準コンテナ仕様になるとみて間違いないと思います。

現状では、WebアプリケーションをJavaで作る場合TomcatやJettyなどの生ServletにSpringなどのフレームワークを載せて使うのが一般的で、Java EEのEJBはあまり使われていません。Java EEからEJBを引くと生Servletになるのであれば、Java EE仕様はあまり関係なくて、今まで通りTomcatやJettyベースでよいことになります。

しかし、実はJava EEのプラットフォームも地味に進歩してきていて、Java EEからEJB(のフルスペックやその他管理系の機能)を引いた部分の機能がかなり大きくなってきており、生Servletとの乖離が大きくなっています。この部分は、Java EEのWeb Profileとして仕様化もされています。Java EEのフルスペックは大変ですが、Web Profileなら使用頻度とのバランス的にリーズナブルな大きさであり、このWeb ProfileがJava VM系のPaaS標準仕様になると予想されるわけです。

ScalaもJava VM上で動作させることが普通なので、Javaプラットフォームの進化とは切っても切れない関係です。そういうわけでScalaでクラウドアプリケーションを書く場合も、Java EEのWeb Profileを念頭に置いておきたいところです。

Web Profileではいくつか重要な機能が追加されていますが、その一つがBean Validationです。Bean Validationは、Java Beansのプロパティを検証する機能です。プロパティ(属性やメソッド)に対するアノテーションで指定された値の値域と実際に格納されている値がマッチしているか検証してくれます。この機能は、普通のJava SEベースのプログラムでも利用したいぐらいの便利な機能で、たまたまJava EEの枠組みで仕様化されていますが、事実上Javaの標準機能といえます。(Jarを追加すればJava SE上でも簡単に使えます。) そういう意味でもScalaプログラミングにも積極的に取り入れたいところです。

さて、ScalaでValidationといえばScalaz Validationですね(笑)。

Scalaz Validationは、applicative functorとして実現されたValidationクラスを中心とした機能で、いわゆるapplicative styleというプログラミングスタイルで、正常系処理と異常系処理を綺麗に取り扱うメカニズムを提供します。

Scalaでクラウドアプリケーションを作る場合、Beans ValidationとScalaz Validationを併用するのが望ましいことはいうまでもありません。そこで、この2つのValidationシステムを統一的に扱うためのプログラミング方法を試行することにしました。

Person

まず、検証対象となるクラスをPersonを定義します。通常のScalaクラスですが、属性に@NotNullや@Size(min=1)といったアノテーションがつけられている点がBean Validationのための追加点です。

  1. class Person(nm: String, ag: Int, ad: Option[String]) {  
  2.   @NotNull @Size(min=1)  
  3.   val name: String = nm  
  4.   @Min(0@Max(150)  
  5.   val age: Int = ag;  
  6.   @NotNull // Option内の判定はできない  
  7.   val address: Option[String] = ad  
  8. }  

本来は、case classを使いたいところですが、コンストラクタの引数につけたアノテーションをBean Validationは認識してくれないみたいなので、泣く泣くこの実装にしています。これは、ScalaからBean Validationを使うときの要注意項目ですね。

また、Optionの中身はBean Validationの基本機能では扱えないので、より本格的に作り込む場合は、カスタムのバリデーターを作る必要があります。

Personのコンパニオンオブジェクト

次に、Personクラスを操作するための関数を集めたコンパニオンオブジェクトPersonを定義します。ここで、Bean ValidationとScalaz Validationを接続する処理を実現します。

以下では、プログラムに直接コメントしていきます。

  1. object Person {  
  2.   // Scalaでは、型名が長くなることが多いので、よく使うものはtypeで定義しておくとよい。  
  3.   type ViolationNelA[A] = NonEmptyList[ConstraintViolation[A]]  
  4.   type ViolationNel = ViolationNelA[Person]  
  5.   type ValidationB[B] = Validation[ViolationNel, B]  
  6.   
  7.   // javax.validationパッケージからBeanValidationのValidatorを取得。  
  8.   val validator = {  
  9.     // Validationがscalaz.Validationと重なるので、BeanValidationという名前で取り込む。  
  10.     import javax.validation.{Validation => BeanValidation}  
  11.     val vf = BeanValidation.buildDefaultValidatorFactory()  
  12.     vf.getValidator()  
  13.   }  
  14.   
  15.   def isValid(p: Person) = validate(p).isEmpty  
  16.   
  17.   // Bean Validatorを使って、Personオブジェクトの検証を行う。  
  18.   // asScala.toListでScalaのListに格納する。  
  19.   // ConstraintViolationはBean Validatorが検出した異常情報。  
  20.   def validate(p: Person): List[ConstraintViolation[Person]] = {  
  21.     validator.validate(p).asScala.toList  
  22.   }  
  23.   
  24.   // 指定された値からPersonオブジェクトを生成する。成功した場合はscalaz.SuccessにPersonオブジェクトを、失敗した場合はscalaz.FailureにConstraintViolationのリストを格納する。  
  25.   // scalaz.Successとscalaz.FailureはScalaz.Validationのサブクラスでそれぞれ検証の成功と失敗を示す。  
  26.   // この関数でBean Validationの結果をScalaz Validation化している。  
  27.   def createV(name: String, age: Int, address: Option[String]): ValidationB[Person] = {  
  28.     val p = new Person(name, age, address)  
  29.     validate(p) match {  
  30.       case vs if vs.isEmpty => p.success  
  31.       // vs.toNel.get.failといったものがScalaz的な書き方。コンパクトに記述できる。  
  32.       case vs => vs.toNel.get.fail  
  33.     }  
  34.   }  
  35.   
  36.   // Bean Validationは、Java Beanに値を設定した後にしか使えない。  
  37.   // Webアプリなどで、生文字列からJava Beansを生成する場合、文字列が適切な値に変換できないため、そもそもJava Beansを生成できないケースもある。そのケースを取り扱うため、文字列からの値変換に失敗した場合はその時点でエラー、Java Beansを生成後はBean Validatorで検証し、いずれの場合も結果はscalaz.Validationで通知する。  
  38.   def createVFromStrings(name: String, age: String, address: String): Validation[NonEmptyList[String], Person] = {  
  39.     // 型名が長くなりがちなので、内部関数やvalで吸収する。  
  40.     // 名前はコメントをつける気持ちでつけるとよい。  
  41.     def parseint(s: String, name: String) = s.parseInt match {  
  42.       case Success(a) => a.success[NonEmptyList[String]]  
  43.       case Failure(e) => violationmsg(name, s, e.getMessage).fail.liftFailNel  
  44.     }  
  45.     def parsestring(a: String, name: String) = a.success[String].liftFailNel  
  46.     def parseoption[T](a: Option[T], name: String) = a.success[String].liftFailNel  
  47.     def constraintviolation2string(cv: ConstraintViolation[Person]) = {  
  48.       violationmsg(cv.getPropertyPath.toString, cv.getInvalidValue.toString, cv.getMessage)  
  49.     }  
  50.     def violationmsg(path: String, value: String, msg: String) = {  
  51.       "%s = %s: %s".format(path, value, msg)  
  52.     }  
  53.     // ロジックの中心は内部関数を使ってコンパクトに書く  
  54.     (parsestring(name, "name") |@| parseint(age, "age") |@| parseoption(Option(address), "address"))(createV(_, _, _)) match {  
  55.       case Success(Success(a)) => a.success[String].liftFailNel  
  56.       case Success(Failure(e)) => e.map(constraintviolation2string(_)).fail  
  57.       case Failure(e) => e.fail[Person]  
  58.     }  
  59.   }  
  60.   
  61.   // List[ValidationB[Person]]をValidationB[List[Person]]に変換。型クラスTraverseと似たような動き。  
  62.   // TraverseでValidationをうまく扱うことができなかったので、foldrで実装してみた。  
  63.   // 「(s <**> p)(_ :: _)」の所がapplicative styleのプログラミング。ValidationがSuccessの場合に走るロジック(正常処理)を記述する。ValidationがFailureの場合(異常処理)は、applicative functorであるValidationのコンテナ側が実装している裏ロジック(?)が走って、monoidとして実現されているエラー情報を蓄積していく。  
  64. // applicative functorのメカニズムを用いることでプログラマが記述する正常系ロジックとValidationが自動的に実行する異常系ロジックを綺麗に分離できる。  
  65.   def sequenceV(persons: List[ValidationB[Person]]): ValidationB[List[Person]] = {  
  66.     persons.foldr(mzero[List[Person]].success[ViolationNel])((s, p) => (s <**> p)(_ :: _))  
  67.   }  
  68. }  

applicative functorはfunctorとmonadの中間に位置する型クラスです。それぞれ計算の文脈(コンテナ)の扱いに違いが出てきます。functorはピュアなアプリケーションロジック実行後ピュアな文脈(コンテナ)を生成、monadはアプリケーションロジックが文脈(コンテナ)を操作するのに対して、applicative functorはピュアなアプリケーションロジックの裏で暗黙的に文脈(コンテナ)が引き継がれていきます。このapplicative functorの有名な応用がValidationで、実際に触ってみるとピュアなアプリケーションロジックをベースに文脈(コンテナ)依存の処理を進められるapplicative styleのプログラミングがなかなか便利なことが分かります。

ValidationMatchers

Bean ValidationをScalaプログラムで扱うときは、ScalaTestのボキャブラリとなるカスタムマッチャーを作っておくと便利です。Bean ValidationとScalaz Validationを併用するので、両方の機能を包含したValidationMatchersを定義することにします。

  1. package advent2011  
  2.   
  3. import org.scalatest.matchers._  
  4. import javax.validation.ConstraintViolation  
  5. import scalaz._  
  6. import Scalaz._  
  7.   
  8. // アプリケーションロジックのためのボキャブラリの追加はtraitの典型的な使い方の一つ。  
  9. trait ValidationMatchers {  
  10.   // Bean Validation用のボキャブラリ  
  11.   def containViolations(violations: List[(String, String)]) = {  
  12.     ContainViolationsMatcher(violations)  
  13.   }  
  14.   
  15.   // Scalaz Validation用のボキャブラリ  
  16.   object success extends ValidationSuccessMatcher  
  17.   def fail(messages: List[String]) = {  
  18.     ValidationFailMatcher(messages)  
  19.   }  
  20. }  
  21.   
  22. // Bean Validation用のマッチャー。  
  23. // BeMatcherやMatcherをextendsして、applyメソッドを定義するだけなので非常に簡単。  
  24. // テスト用のボキャブラリを簡単に追加できる。  
  25. case class ContainViolationsMatcher(violations: List[(String, String)]) extends BeMatcher[List[ConstraintViolation[_]]] {  
  26.   def apply(value: List[ConstraintViolation[_]]) = {  
  27.     def iscontain(nm: (String, String)) = {  
  28.       val (name, message) = nm  
  29.       value.any(v => v.getPropertyPath.toString == name && v.getMessage == message)  
  30.     }  
  31.     // allはscalazの型クラスFoldableの関数。関数名に∀の記号を使うこともできる。  
  32.     val result = violations.all(iscontain)  
  33.     MatchResult(result, "does not contains expected violation""contains expected violation")  
  34.   }  
  35. }  
  36.   
  37. // Scalaz Validation用のマッチャー。Successの判定をする。  
  38. case class ValidationSuccessMatcher() extends Matcher[Validation[NonEmptyList[String], _]] {  
  39.   def apply(value: Validation[NonEmptyList[String], _]) = {  
  40.     val result = value.isSuccess  
  41.     MatchResult(result, "failure""success")  
  42.   }  
  43. }  
  44.   
  45. // Scalaz Validation用のマッチャー。Failureの判定をする。  
  46. case class ValidationFailMatcher(messages: List[String]) extends Matcher[Validation[NonEmptyList[String], _]] {  
  47.   def apply(value: Validation[NonEmptyList[String], _]) = {  
  48.     value match {  
  49.       case Success(a) => MatchResult(false"incorrect success""")  
  50.       case Failure(e) => if (e.all(messages.contains)) {  
  51.         MatchResult(true"""correct failure")  
  52.       } else {  
  53.         MatchResult(false"incorrect failure""")  
  54.       }  
  55.     }  
  56.   }  
  57. }  

PersonSpec

最後に、Personの使い方をScalatestのWordSpecで書いてみました。WordSpecはBDD(Behavior Driven Development)スタイルのSpecを記述するためのクラスです。Scalatest標準のShouldMatchersに加えて、先ほど作成したValidationMatchersのボキャブラリを追加しています。
ScalatestのBDDは、テスト用のアプリケーションロジックと結果判定がコーディングスタイル上明確に分離できるので、プログラムの視認性が高くなります。また、カスタムマッチャーを作り足すことで、より英文っぽい記述が可能になるので、そのあたりの遊び的な要素がプログラミングを進める上で良い感じです。

  1. class PersonSpec extends WordSpec with ShouldMatchers with ValidationMatchers {  
  2.   "A Person" should {  
  3.     "provide isValid and validate operation" that {  
  4.       "against valid Person" in {  
  5.         val p = new Person("taro"30"Yokohama".some)  
  6.         Person.isValid(p) should be (true)  
  7.         Person.validate(p) should be === Nil  
  8.       }  
  9.       "against invalid Person" in {  
  10.         val p = new Person("", -150null)  
  11.         val expected = List("address" -> "may not be null",  
  12.                             "name" -> "size must be between 1 and 2147483647",  
  13.                             "age" -> "must be greater than or equal to 0")  
  14.         Person.isValid(p) should be (false)  
  15.         Person.validate(p) should have length (expected.size)  
  16.         // 追加したボキャブラリを使用  
  17.         Person.validate(p) should be (containViolations(expected))  
  18.       }  
  19.     }  
  20.     "provide createV to create Person with Validation." that {  
  21.       // アプリケーションロジックを普通のScalaプログラム的に書いた場合。  
  22.       "Plain usage." in {  
  23.         val taro = Person.createV("taro"30, Some("Yokohama"))  
  24.         val hanako = Person.createV("hanako"25, Some("Kamakura"))  
  25.         if (taro.isSuccess && hanako.isSuccess) {  
  26.           val tage = taro match {  
  27.             case Success(p) => p.age  
  28.           }  
  29.           val hage = hanako match {  
  30.             case Success(p) => p.age  
  31.           }  
  32.           val avg = (tage + hage) / 2.0  
  33.           avg should be (27.5)  
  34.         } else {  
  35.           sys.error("invalid")  
  36.         }  
  37.       }  
  38.       // アプリケーションロジックをScalazのapplicative styleで書いた場合。  
  39.       // よりコンパクトで分かりやすく記述できる。  
  40.       "Scalaz usage, applicative style." in {  
  41.         val taro = Person.createV("taro"30, Some("Yokohama"))  
  42.         val hanako = Person.createV("hanako"25, Some("Kamakura"))  
  43.         val avgv = (taro <**> hanako)((x, y) => (x.age + y.age) / 2.0)  
  44.         // この段階までValidationの文脈(コンテナ)の上で計算が進んでいる。  
  45.         // 以下のmatch式で、アプリケーションロジックが正常に動作した場合と、エラーがある場合を分離して、それぞれのロジックを記述している。  
  46.         avgv match {  
  47.           case Success(avg) => avg should be (27.5)  
  48.           case Failure(e) => sys.error("invalid")  
  49.         }  
  50.       }  
  51.     }  
  52.     "provide createV and sequenceV for applicative style." that {  
  53.      // ValidationのListを扱う場合2例。いずれもapplicative style。  
  54.       "Use sequenceV to convert List[Validation[Person]] to Validation[List[Person]] " in {  
  55.         val taro = Person.createV("taro"30, Some("Yokohama"))  
  56.         val hanako = Person.createV("hanako"25, Some("Kamakura"))  
  57.         val jiro = Person.createV("jiro"35, Some("Tokyo"))  
  58.         val persons = List(taro, hanako, jiro)  
  59.         val personsv = Person.sequenceV(persons)  
  60.         val avgv = personsv.map(x => x.map(_.age).sum.toFloat / x.length)  
  61.         avgv match {  
  62.           case Success(avg) => avg should be (30.0)  
  63.           case Failure(errors) => sys.error("invalid")  
  64.         }  
  65.       }  
  66.       "Use foldl to sum of age" in {  
  67.         val taro = Person.createV("taro"30, Some("Yokohama"))  
  68.         val hanako = Person.createV("hanako"25, Some("Kamakura"))  
  69.         val jiro = Person.createV("jiro"35, Some("Tokyo"))  
  70.         val persons = List(taro, hanako, jiro)  
  71.         val sumv = persons.foldl(0.success[Person.ViolationNel])((s, p) => (s <**> p)(_ + _.age))  
  72.         val avgv = sumv.map(_.toFloat / persons.length)  
  73.         avgv match {  
  74.           case Success(avg) => avg should be (30.0)  
  75.           case Failure(errors) => sys.error("invalid")  
  76.         }  
  77.       }  
  78.     }  
  79.     "provide createVFromStrings to create Person from plain strings." that {  
  80.       "Valid parameters." in {  
  81.         val person = Person.createVFromStrings("taro""30""Yokohama")  
  82.         // 追加したボキャブラリを使用  
  83.         person should success  
  84.       }  
  85.       "Invalid parameters of type mismatch." in {  
  86.         val person = Person.createVFromStrings("""a""Yokohama")  
  87.         val expected = List("""age = a: For input string: "a"""")  
  88.         // 追加したボキャブラリを使用  
  89.         person should fail(expected)  
  90.       }  
  91.       "Invalid parameters of invalid value." in {  
  92.         val person = Person.createVFromStrings("""30""Yokohama")  
  93.         val expected = List("""name = : size must be between 1 and 2147483647""")  
  94.         // 追加したボキャブラリを使用  
  95.         person should fail(expected)  
  96.       }  
  97.     }  
  98.   }  
  99. }  
実行結果は以下のようになります。トップレベル, that, inの三層でテストを整理できるのがなかなか便利です。
[info] PersonSpec:
[info] A Person 
[info]   should provide isValid and validate operation that 
[info]   - against valid Person
[info]   - against invalid Person
[info]   should provide createV to create Person with Validation. that 
[info]   - Plain usage.
[info]   - Scalaz usage, applicative style.
[info]   should provide createV and sequenceV for applicative style. that 
[info]   - Use sequenceV to convert List[Validation[Person]] to Validation[List[Person]] 
[info]   - Use foldl to sum of age
[info]   should provide createVFromStrings to create Person from plain strings. that 
[info]   - Valid parameters.
[info]   - Invalid parameters of type mismatch.
[info]   - Invalid parameters of invalid value.
[info] Passed: : Total 9, Failed 0, Errors 0, Passed 9, Skipped 0
[success] Total time: 2 s, completed 2011/12/23 11:54:09

まとめ

Bean ValidationとScalaz Validationを併用する方法について試行してみました。
Bean Validation用のアノテーションとScalaの相性に若干問題があるようですが、プログラミング的には特に問題なくシームレスに繋げることが確認できました。

また、Scalaz Validationの実現技術であるapplicative functorによるapplicative styleによるプログラミング、ScalatestによるBDDという技術も合わせて使ってみました。いずれもJavaでは実用化が難しい技術で、Scalaを使うメリットですね。

今回使用した技術は以下のものになります。合わせてプログラミングしてみてJava EE web profile技術、Scalaz、ScalaTestによるTDD/BDDといったところがScalaプログラミングのベースになりそう、という感を強くしました。

  • Bean Validation
  • Scalaz Validation
  • Scalaz applicative functor (applicative style)
  • Scalatest BDD
  • Scalatest カスタムマッチャー

2011年12月14日水曜日

ImmutableとBuilder

このエントリはJava Advent Calender 2011の第14日目です。

OOPで最も重要なテクニックは何でしょうか。色々な考え方があるでしょうが、ボクはImmutable Objectがまず最初に浮かびます。
かなり前にJava World誌に書いていたJavaデザインノートという連載でも、第1回のテーマはImmutable Objectでした。

Immutable Object(不変オブジェクト)は、生成時に属性を設定した後、属性値を変更することができないオブジェクトです。Javaの場合、StringやDateなどの情報を保持する基本クラスの多くがImmutable Objectになっており、Javaプログラミングを下支えしています。

Immutable Objectの長所は、とにかく属性値が変更されないこと。一度生成したオブジェクトは何の心配もなく、メソッドの引数で渡すことができます。その先で、どのような共有のされかたをしても、知らないところで勝手に内容が変更されるということがありません。マルチスレッドプログラミングでは、この性質はさらに重要になります。

一方、Immutable Objectは以下の短所があります。

  1. 値を変更するためには、オブジェクトをまるごと複写する必要がある。
  2. すべての値の設定をオブジェクト生成時に行う必要があり、プログラミングがちょっと煩雑。

前者の問題は、最近のハードウェア性能の向上で、普通のWebアプリ、業務アプリではまず問題にはならないでしょう。まるごと複写といってもshallow copyとなるので、まるまるデッドコピーとはなるわけではありません。 また、多少の性能劣化があっても、プログラム品質、開発効率の向上の方がより重要というケースは多いはずです。 よほど性能要件の高い場合でも適材適所で使い分ければよいわけです。

そういう意味で、Value Objectは当然として、DTOといった情報量の多いオブジェクトもできるだけImmutable Object化していきたいところです。

となると、現在問題となるのは後者。まずオブジェクト生成時のコンストラクタで全情報を引数で渡さなければなりません。さらに、オブジェクトの変更(つまりデータを変更して複写)では、オブジェクトのほとんどの情報と変更したいデータを用意して、コンストラクタに渡すという手間が必要になります。 StringやDateのようなValue Objectでは、オブジェクトが管理する情報が1つやせいぜい2つなので全く問題ありませんが、DTO等の場合は管理する情報が数十個になるケースも考えられ、かなり煩雑になります。数十個と言わずとも5個ぐらいでも十分嫌になります。実際のプログラミングではこの手間が嫌で、Immutableの方がよいと分かっていても普通のMutable Objectを選択することになりがちです。これを何とかしたいですね。

この問題を解決するのがBuilderです。

プログラムはこんな感じ。

Immutable Object「Person」と、そのBuilderである「Person.Builder」を定義しています。 Personは、Immutable Objectであることを活用して属性をpublicにして外から扱いやすくしています。 Person.Builderの方は、普通のMutableオブジェクトで、値を設定しやすくするための工夫を好きなだけ行うことができます。ここではwithXXXメソッドでメソッドチェインを実現してみました。

  1. package advent2011;  
  2.   
  3. import java.util.List;  
  4. import java.util.ArrayList;  
  5. import static java.util.Collections.unmodifiableList;  
  6.   
  7. public class Person {  
  8.     public final String name;  
  9.     public final int age;  
  10.     public final String address;  
  11.     public final List<String> phones;  
  12.   
  13.     public Person(String name, int age, String address,  
  14.                   List<String> phones) {  
  15.         this.name = name;  
  16.         this.age = age;  
  17.         this.address = address;  
  18.         this.phones = unmodifiableList(new ArrayList<String>(phones));  
  19.     }  
  20.   
  21.     public static class Builder {  
  22.         public String name;  
  23.         public int age;  
  24.         public String address;  
  25.         public ArrayList<String> phones = new ArrayList<String>();  
  26.   
  27.         public Builder() {  
  28.         }  
  29.   
  30.         public Person build() {  
  31.             return new Person(name, age, address, phones);  
  32.         }  
  33.   
  34.         public Builder withName(String name) {  
  35.             this.name = name;  
  36.             return this;  
  37.         }  
  38.   
  39.         public Builder withAge(int age) {  
  40.             this.age = age;  
  41.             return this;  
  42.         }  
  43.   
  44.         public Builder withAddress(String address) {  
  45.             this.address = address;  
  46.             return this;  
  47.         }  
  48.   
  49.         public Builder withPhone(String phone) {  
  50.             this.phones.add(phone);  
  51.             return this;  
  52.         }  
  53.     }  
  54. }  

こんな感じで使います。

  1. package advent2011;  
  2.   
  3. import org.junit.Test;  
  4. import static org.hamcrest.core.Is.is;  
  5. import static org.junit.Assert.*;  
  6.   
  7. public class PersonTest {  
  8.     @Test  
  9.     public void buildPerson() {  
  10.         Person.Builder builder = new Person.Builder();  
  11.         builder.withName("Taro")  
  12.             .withAge(30)  
  13.             .withAddress("Yokohama")  
  14.             .withPhone("045-123-4567");  
  15.         Person person = builder.build();  
  16.         assertThat(person.name, is("Taro"));  
  17.         assertThat(person.age, is(30));  
  18.         assertThat(person.address, is("Yokohama"));  
  19.         assertThat(person.phones.get(0), is("045-123-4567"));  
  20.         System.out.println(person);  
  21.     }  
  22. }  

Personオブジェクトを生成す時の手間は、普通のMutableオブジェクトの時と同じ。 buildメソッドで返ってくるPersonオブジェクトはImmutableオブジェクトなので、安全に使うことができます。 どちらの目的も叶える解ですね!

新たな問題

さて、Builderを使うとImmutableオブジェクトが使いやすくなるのは分かったのですが、新たに別の問題が発生します。

このBuilderを用意する手間がなかなか大変なんです。コーディング量も多いですし、コーディング内容が単純作業のためが短調で面白くない、という問題もあります。 この手間があるので、このままでは結局使わないテクニックになってしまいそうです。

単調なコーディングあるところ、自動生成あり。ここで登場するのが自動生成です。クラスの形が分かっていれば、その情報からJavaプログラムのソースコードを生成すればよることが可能です。

SimpleModelerService

現在開発中のSimpleModelerServiceでは、この問題に対応するためにDSLからJavaプログラムを自動生成する機能を提供する予定です。DSLにはScala DSLに加えて、CSV、マインドマップ(XMind)、Excelなどが利用できるようになる予定です。 今回は、CSVを使って試してみましょう。

以下の内容のCSVファイルを用意します。

  1. person,name;age(int);address?;phone+  

CSVの第1カラムにクラス名、第2カラムに属性のリストを定義しています。「(int)」はint型、「?」「+」は多重度(?は0または1、+は1以上)を意味しています。

これを以下のフォームに設定して「生成」すると、色々なコードが生成されたものをzipで固めたものが返ってきます。

この中にある、DDPerson.javaに今回のテーマであるImmutableオブジェクトとBuilderが記述されています。ぜひ覗いてみてください。

ファイル名:

ImmutableオブジェクトとそのBuilderの作成は、手間がかかるのでつい手抜きをしたくなりますが、プログラムの自動生成を活用すれば楽々クリアできますね。
SimpleModelerのJava生成機能は、近々公開したいと思いますので、ぜひご利用ください。

2011年12月7日水曜日

SimpleModelerServiceのアーキテクチャ

昨日公開したXMind→クラス図変換サービスは、g3上に構築したSimpleModelerServiceというRESTサービスです。
このRESTサービスはScalaでプログラミングし、WAR形式にパッケージングしたものをGlassFish上で動作させています。
Java系のPaaS標準コンテナは、WAR形式をJavaEEのWebプロファイル+αを動作させるものになると予想されるので、この環境をScala&自作フレームワークで試してみるというという目的もありました。

SimpleModelerServiceは以下の4つのモジュールから構成されています。

Goldenport
アプリケーションフレームワーク
g3
クラウドアプリケーションフレームワーク
SimpleModeler
モデルコンパイラ
SimpleModelerService
SimpleModelerサービス

SimpleModelerServiceが最上位にあるクラウドアプリケーションの本体で、g3上に構築されています。SimpleModelerServiceはgoldenport上に構築されたスタンドアロンアプリケーションであるSimpleModelerをRESTサービス化します。

SimpleModelerServiceの開発規模は以下の表になります。Scalaが約121.6Ks、Javaが約10.8Ksです。Scalaのコーディング量が100Kステップを超えてきておりちょっと感慨深いものがあります。

開発規模
モジュールScala(Ks)Java(Ks)
Goldenport16.89.1
g322.61.7
SimpleModeler82.1N/A
SimpleModelerService0.1N/A

SimpleModelerService on g3

SimpleModelerServiceでは、クラウドアプリケーション用フレームワークとして開発してきたg3を実応用に初めて適用することができました。

g3はScala DSLでアプリケーションを記述しますが、SimpleModelerServiceは以下のものになります。

  1. class SimpleModelerService extends G3Application {  
  2.   title = "SimpleModeler"  
  3.   summary = <div>SimpleModeler service produces various artifacts from a SimpleModeling model.</div>port("/diagram",  
  4.       Description(  
  5.           "Diagram""SimpleModeler Diagram Service",  
  6.           <div locale="en">SimpleModeler diagram service produces a class diagram from a mindmap modeled by MindmapModeling.</div>,  
  7.           Schema(  
  8.               (Symbol("source.package"), XString, MZeroOne),  
  9.               ('_1, XBase64Binary)))  
  10.   ) agentpf {  
  11.     case p: Post => Post("diagram", p)  
  12.   } invoke('sm)  
  13.   
  14.   goldenport('sm, SimpleModelerDescriptor)  
  15. }  

port("/diagram")はHTTPリクエストを受け取るポートを指定しています。
URIの断片"/diagram"にマッチしたHTTPリクエストがこのポートで受信され、パイプラインで以下の処理が実行されていきます。

  1. agentpfでPostリクエストを加工するPartialFunctionを実行。
  2. invokeでチャネルsmを呼出し。

その下にあるgoldenport('sm, SimpleModelerDescriptor)は、チャネルsmをGoldenportアプリケーションSimpleModelerに割り当てる設定です。SimpleModelerのGoldenport DSLであるSimpleModelerDescriptorを指定しています。

port("/diagram")からinvokeでこのsmチャネル経由でSimpleModelerが実行され、その実行結果がport("/diagram")の実行結果としてクライアントに返されます。

HTTPのプロトコル処理、FormやJSON、AtomPub, MIMEといったデータ入出力、ファイルのアップロード処理はg3フレームワークが行います。アプリケーションは、GetやPostといったメッセージに対する関数型的な転換処理の連鎖として、アプリケーションロジックを記述することができます。また、Goldenport上に構築したスタンドアロンアプリケーションは、シームレスに接続できるようになっており、ほぼそのままRESTサービス化することができます。

SimpleModeler on Goldenport

モデルコンパイラはアプリケーションフレームワークGoldenport上に構築しています。そのアーキテクチャを定義するDSLが以下のSimpleModelerDescriptorです。

  1. class SimpleModelerDescriptor extends GApplicationDescriptor {  
  2.   name = "SimpleModeler"  
  3.   version = "0.3.0"  
  4.   version_build = "20111206"  
  5.   copyright_years = "2008-2011"  
  6.   copyright_owner = "ASAMI, Tomoharu"  
  7.   command_name = "sm"  
  8.   //  
  9.   classpath("target/classes")  
  10.   importers(ScalaDslImporter)  
  11.   entities(CsvEntity, XMindEntity, OpmlEntity, ExcelTableEntity,   
  12.       OrgmodeEntity, YamlEntity)  
  13.   services(ProjectRealmGeneratorService,  
  14.     ImportService,  
  15.     ConvertService,  
  16.     HtmlRealmGeneratorService,  
  17. //    JavaRealmGeneratorService,  
  18.     GrailsRealmGeneratorService,  
  19.     GaeRealmGeneratorService,  
  20.     GaeoRealmGeneratorService,  
  21.     GaeJavaRealmGeneratorService,  
  22.     AndroidGeneratorService,  
  23.     G3GeneratorService,  
  24.     AsakusaGeneratorService,  
  25.     DiagramGeneratorService)  
  26. }  

SimpleModelerDescriptorでは概ね以下のような定義を行っています。

  • importersで外部データの移入器としてScalaDslImporterを指定。
  • entitiesでCsvEntityやXMindEntityなどを指定。サフィックスがcsvやxmlになっているファイルを入力すると、自動的にCSVEntityやXMindEntityに変換される。
  • servicesでDiagramGeneratorServiceやAndroidGeneratorServiceなどを指定。パラメタにより自動的に適合するサービスが起動される。

Goldenportが外部入出力のもろもろをハンドリングしてくれるので、アプリケーションロジックの記述に専念することができます。g3を併用することでRESTサービスまでシームレスに接続できるようになりました。

ScalaとDSL

DSLの用途は大きく(1)モデルなどの静的な情報を記述、(2)フレームワークのAPIを記述、の2つに分けられます。

SimpleModelerがモデル記述に使っているScala DSLは前者の例です。

一方、本記事で取り上げたSimpleModelerService(g3)、SimpleModeler(Goldenport)は後者の例です。SimplModeler(Goldenport)のDSLは、Javaでも何とか実現できそうですが、SimpleModelerServiceのDSLはJavaではこのような記述は難しく、DSLを得意とするScalaの美点がよく出ています。

Scalaは元々、JavaでのDSL実現に限界を感じていたため、DSL用の言語として採用したのですが、十分満足できる結果を得られました。Scalaで100K超のコーディングを行った事になりますが、Javaと比べてプログラミングが圧倒的に楽ということも体感できました。

Goldenportやg3のようなDSLベースのAPIを持つフレームワークやコンポーネントがこれからどんどん出てくることが予想されるので、Scalaプログラミングの生産性はますます向上してくることになるでしょう。また、メニーコア時代の並行プログラミングは関数型言語を中心に広がっていきそうです。今回確認できたようにGlassFishのようなJava用のクラウドコンテナ(の候補)にもまったくシームレスに載せることができるのは、Javaとの互換性を軸に据えているScalaならではです。色々考えていくとScalaはかなり便利な言語で、クラウドアプリケーション向けプログラミング言語の最右翼かなと実感しています。

2011年12月6日火曜日

XMindをクラス図に変換するクラウドサービス

XMind形式のマインドマップモデルからクラス図(PNG)を生成するクラウドサービスを作成しました。
次回の横浜モデリング勉強会(1月21日(土)予定)で使用する予定です。 作成中のマインドマップをクラス図に変換して確認することによって、マインドマップモデルとオブジェクトモデルの関係をより深く理解できるようになると思います。

ツールをインストールするのは何かと大変なので、開発中のツールはこういったクラウドサービスで提供していく予定です。

このツールは、CURLコマンドまたはWeb上に置いたガジェット(HTML断片)を使って使用することができます。

Bloggerで、このガジェットが動くのか試すために以下で配置してみました。もしかしたらここから動かすことができるかもしれません。

ファイル名:

クラウドサービスの配備には、さくらインターネットとGlassFishを使ってみました。
この組合せは、かなり簡単にScalaアプリケーションが動きますね。
どちらのUIもこなれていて作業のハードルが低いので、気軽に色々試してみたいと思います。

追記:Bloggerからもガジェット動きました。サービスはまだまだ柔らかいので、変なことをすると落ちるかもしれません。お手柔らかに。

2011年12月2日金曜日

要約エンティティ

きのう「Deep Hadoop Night~誰がHadoopを殺したか?~」を聞いてきました。タイトルからも分かるように刺激的な内容でしたがオフレコが趣旨とのことで内容への感想は差し控えますが、それはともかく、座談会のお話に色々なヒントが満載で、クラウドアプリケーションのモデリングを考える上でとても刺激になりました。

Hadoop的なサービスが普通のインフラとして整備された世界での、クラウドアプリケーションのモデリング手法について考察が進みました。今まで考えていたアイデアが少しまとまってきたので、つらつら、まとめておきたいと思います。

データモデル

以下の図は、ここ数年、クラウドアプリケーションのメタモデルの素案として使用しているものを今回のアイデアに基づき改良したものです。



基本的にはクラウド以前に使っていたメタモデルを自然に拡張したものになっています。概念モデルや分析モデルのあたりは、要するに利用者などのステークホルダー観点でのモデルを構築することになるので、クラウドプラットフォームだからといって、特別な手法を取り込むということにはならないと考えています。

もちろん、クラウドによって今まであったシステム構築上の制約が外れてくるので、そのあたりで新たに考慮に入れる必要がある要因は増えてきます。

このメタモデルは、2008年頃からクラウド向けに徐々に改良してきたもので、クラウド向けのモデル要素として以下のものを追加してきました。

  • アプリケーション利用モデルのUX(User Experience)
  • アプリケーション実現モデルのメッセージフロー
UX(User Experience)は、UCD(User Centered Design)によるUIモデリング技術の発展とGUI技術の向上を取り込む意図です。UXをシステムに落し込む時の技術としてユースケースを組み合わせることを考えています。ユースケースのカバー範囲が少し小さくなるとともに、ユースケースをシステム設計側のモデリング技術として再構築することを想定しています。
メッセージフローは、さまざまなシステム間でのメッセージ交換によって協調動作するクラウドアプリケーションのアーキテクチャを、制御フローとデータフローを統合した形で記述する目的のものです。ただし、システム設計レベルの詳細な記述力は持たないので、プログラムの自動生成といった用途には使用できません。
ここまでは、以前からあちこちでお話ししている内容なのですが、これに加えて、今回新たなモデル要素として「要約エンティティ」を追加することにしました。この「要約エンティティ」がクラウドアプリケーションのモデリングの要になるのでは、と思いついたわけです。

ドメインモデル

分析モデル、設計モデルの軸となるのがドメインモデルです。(以下、正確には振舞いを含んだオブジェクトモデルですが、イメージしづらいのでデータモデルとして考えていきます。)
ポイントとなるのは、一つのドメインモデルでUIから永続データまで直接カバーするのではなく、ドメインモデルの枠組みの中で、目的毎のデータモデルを構築して、これらのデータモデルを連携させて、全体として機能させていく必要があるのではないかという仮定です。
その目的のために、仮にドメインモデルを以下のような要素に分解して実現することを考えます。
  • メンタルデータモデル
  • アプリケーションデータモデル
  • 永続データモデル
  • 集約データモデル
UXの実現は、利用者の目的を達成するためという軸から導きだされるものなので、永続データモデルに引きづられすぎては本末転倒です。UX専用のメンタルデータモデルを構築します。
クラウドアプリケーションでは、複数のデータソースから得られた情報を取捨選択射影して、アプリケーション独自のデータモデルを構築します。アプリケーションの意図に沿っていること、色々なデータソースを融合させるための十分な記述力を持っていること、メンタルデータモデルへの写像が行えること、といった要件を満たす必要があります。

要約エンティティ

利用者側からは利用者中心のメンタルモデル、クラウドプラットフォームからは外部データを集約データモデル、さらにアプリケーションがやりたい事を実現するためのアプリケーションデータモデル。このようないくつかのモデルを連携させて仕事をさせるには、ハブとなるモデル要素を軸に据えるのが常道です。
このモデル要素として、以前からアイデアとして温めていた要約エンティティ(ステレオタイプはsummery)がぴったりハマるのではないかというのが、今回の気付きになります。
要約エンティティは、アプリケーションが必要なさまざまな情報を集計、集約したエンティティです。つまり、元ネタとなるファクトモデルのエンティティではなく、そこから二次的に派生したエンティティとなります。
ある意味では、データベースのビューと同じ目的のものですが、問合せをカプセル化したものではなく、アプリケーションの意志で情報を集積、計算、追加していく点が異なります。

事前計算

クラウドアプリケーションでは、利用者数、データ規模の双方に対するスケーラビリティが問題となるアプリケーションが当然増えてきます。
スマートデバイスからのアクセスでは、より高速なレスポンスが要求されます。
クラウドアプリケーションの場合は、アプリケーションの外部にあるさまざまなデータを取り込んで利用するユースケースも考えられます。この場合は、外部のアプリケーションへの通信が発生するため、相応の遅延が発生することになります。複数の外部アプリケーションと連携する場合はさらに大変なことになります。
いずれにしても、問合せを受けた契機で、アプリケーションロジックとして問合せ処理を行うのでは、実用的な性能がでない可能性が高いでしょう。
この問題へ対応するには、予測される問合せ処理を事前にバックグラウンドで行っておく必要があります。この事前計算の結果を格納する受け皿として要約エンティティが使用できます。
以下の図ではドメインイベントを契機に、ドメインデータの値と合わせて、何らかの要約処理を行い要約エンティティに格納しています。このように、利用者が問合せしたタイミングではなく、イベントが発生したタイミングで要約データを計算しておくことで、利用者からの問合せに対して高速にレスポンスを返すことができるようになります
要約エンティティを導入すると、各種データのスムーズな連携が可能になるのに加えて、問合せ結果を事前計算しておくことによって、問合せのレスポンス時間を向上させるという、クラウドアプリケーションで頻出するユースケースを自然に実現できるようになります。
要約データを用いて問合せの性能を上げる手法は以前からあるわけですが、クラウドアプリケーションの場合はより重要な手法になります。
要約データは、ある意味ではデータモデルの論理的な一貫性を弱くしてしまうデータを導入することになります。このため、今までは、要約データはできるだけ用いずトランザクションの枠組みの中でSQLによって一括に結合して取得するアプローチが好まれていました。
しかし、クラウドアプリケーションのスケーラビリティ要件では、このような強い一貫性を用いるとレスポンス時間に大きな犠牲が出てくるため、逆に一貫性を弱め、いかに適切な事前計算をしておくことによって、実用的なレスポンス時間を確保するのかという点が、重要なアプローチになるわけです。

イベント駆動

一般的には、アクターは端末を操作するエンドユーザーであることが多いわけですが、クラウドアプリケーションでは、事情が変わってきます。
クラウドアプリケーションでは、今まで以上に他システムとの連携が重要になります。他システムが提供するサービスをRESTなどのプロトコルで使用し、コアコンピタンスの処理のみをアプリケーションで実装することになるでしょう。
この他システムもアクターとしてモデル化します。
外部センサーやスマートデバイスもクラウドアプリケーションと連携するアクターです。
また、アクターに外部センサーやスマートデバイスを加えて、ここから上がってくるイベントのコラボレーションをモデルに加えていけばよいでしょう。
アクターが利用者の場合には、基本的には利用者からの問合せや申請に対応するというアプリケーションアーキテクチャになります。
しかし、クラウドアプリケーションでは、利用者からの問合せや申請に加えて、他システムからプッシュされてくるさまざまなイベントを蓄積、集計して刻一刻とアプリケーションデータを更新していくことになります。
また、上で説明したように利用者からの問合せを予測して事前計算しておくことで、より高速に問合せに対するレスポンスを行うというニーズもあります。
このような要件に対応するために、上がってきたイベントをイベントエンティティといったファクトモデルとして記録すると同時に、アプリケーションのユースケースに合わせた要約エンティティにも集計を行い、事前計算をしておくことが有効になると考えられます。
また、最近注目されているストリーミングなども、イベント駆動の粒度が小さくなったものと考えることができます。この場合も要約エンティティが有効に機能するはずです。

まとめ

要約データ(summary)自体は、新しいアイデアではなくて、大きめのシステムでは広く用いられているものと思いますが、データモデルの正規化という観点からは、あまり望ましいものではなく、どちらかというと日陰者的な扱いではないかと思います。これを一級市民として扱って、UXとイベント駆動を結びつけるハブとして使うと、ぴったり収まるのではないか、というのが今回のアイデアです。
設計段階の回避策ではなく、分析段階から積極的に使っていこうということですね。そうすることによって、クラウドにまつわる遅延や障害、スケーラビリティというさまざまな問題をうまく扱えるのではないかということです。こうやって文章にしてみると、地味な改良案ですが、メタモデラー的には面白い気付きになりました。