忘年会駆動で 「バリバリ、検証は任せろー。やめて!」 の話しをしてきました

12/28 の 忘年会駆動 - 2013 に 2,3 日前に誘われたので、急いで資料作って話してきました。
何の話しかと言うと Java の Bean Validation のお話しです。


資料の中で書いているコードは、Xtend - Modernized Java です。

資料の中で紹介出来なかったコードの一部はこんなのです。
こんな 売上 と 売上明細 があった時に、

package sample

import java.util.Date
import java.util.List

class 売上 {
  @Property Integer 売上PK
  @Property Integer 得意先PK
  @Property String 得意先名
  @Property Integer 請求先PK
  @Property String 請求先名
  @Property Date 売上日
  @Property List<売上明細> 明細
  def int get合計金額() {
    明細?.fold(0) [total, item | 
      total + item.金額
    ] ?: 0
  }
}
class 売上明細 {
  @Property int 連番
  @Property Integer 商品PK
  @Property String 商品名
  @Property int 数量
  @Property int 単価
  def int get金額() {
    数量 * 単価
  }
}

売上登録 と 売上確定 を行う処理を書くとします。
当然ながら 売上登録 と 売上確定 では、必要とする項目や、検証内容が異なります。
という訳で interface を用意します。

package sample

import java.util.Date
import java.util.List
import javax.validation.Valid
import javax.validation.constraints.Max
import javax.validation.constraints.Min
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size

interface 売上登録 {
  @NotNull(groups=売上登録)
  def Integer get得意先PK()
  @NotNull(groups=売上登録)
  def Integer get請求先PK()
  @NotNull(groups=売上登録)
  def Date get売上日()
  @NotNull(groups=売上登録)
  @Size(min=1, groups=売上登録)
  @Valid
  def List<? extends 売上明細登録> get明細()
  @Max(value=100000, groups=売上登録)
  @Min(value=-100000, groups=売上登録) 
  def int get合計金額()
}
interface 売上明細登録 {
  @Min(value=1, groups=売上明細登録)
  def int get連番()
  @NotNull(groups=売上明細登録)
  def Integer get商品PK()
  def int get数量()
  def int get単価()
  @Max(value=10000, groups=売上明細登録)
  @Min(value=-10000, groups=売上明細登録) 
  def int get金額()  
}

interface 売上確定 {
  @NotNull(groups=売上確定)
  def Integer get売上PK()
  def int get合計金額()
}

売上 と 売上明細に implements します。

package sample

import java.util.Date
import java.util.List

class 売上 implements 売上登録, 売上確定 {
  @Property Integer 売上PK
  @Property Integer 得意先PK
  @Property String 得意先名
  @Property Integer 請求先PK
  @Property String 請求先名
  @Property Date 売上日
  @Property List<売上明細> 明細
  override get合計金額() {
    明細?.fold(0) [total, item | 
      total + item.金額
    ] ?: 0
  }
}
class 売上明細 implements 売上明細登録 {
  @Property int 連番
  @Property Integer 商品PK
  @Property String 商品名
  @Property int 数量
  @Property int 単価
  override get金額() {
    数量 * 単価
  }
}

登録処理や確定処理はこんな感じ。

package sample

import java.util.Set
import javax.validation.ConstraintViolation
import javax.validation.Validation

class 売上Service {
  private def <T> void throwIfInvalid(
    Set<ConstraintViolation<T>> errors) {

    if (errors === null || errors.empty) return
    val ex = new Exception(errors.map [
      '''«propertyPath.toString»:«message»'''
    ].join("\n"))
    throw ex
  }
  def Integer 登録(売上登録 target) {
    val validator = Validation.buildDefaultValidatorFactory.validator
    // 検証エラーの時は、とりあえず例外にしとく
    validator.validate(target, 売上登録, 売上明細登録).throwIfInvalid
    // 正常ケース とりあえず1返す
    val new売上PK = 1
    new売上PK
  }
  def void 確定(売上確定 target) {
    val validator = Validation.buildDefaultValidatorFactory.validator
    // 検証エラーの時は、とりあえず例外にしとく
    validator.validate(target, 売上確定).throwIfInvalid
    // 正常ケース
  }
}

使う時は、

package sample

import java.util.Date

class Main {
  def static void main(String... args) {
    val 売上 = new 売上 => [
      得意先PK = 1
      得意先名 = "商店"
      請求先PK = 1
      請求先名 = "商店"
      売上日 = new Date
      明細 = #[
        new 売上明細 => [
          連番 = 1
          商品PK = 100
          商品名 = "小物"
          数量 = 5
          単価 = 10
        ]
      ]
    ]
    val service = new 売上Service
    売上.売上PK = service.登録(売上)
    service.確定(売上)
  }
}

interface を用意すると、必要な項目しか IDE の入力補完に出てこない。
かつ、それぞれの処理で必要な検証内容もまとまってて見やすい気がする。
欠点は、interface 書くのがめんどい。*1
interface の代わりに、値の移し替えが必要だけど immutable な DTO とかでも良いかも。

*1:特に groups