Design by Contract(契約による設計)でScalaの守備力を上げる
このエントリはScala Advent Calendar jp 2010の16日目です。
昨日は@ussy00さんのScala でテンプレートエンジンを利用して HTML メールを送信するでした。
月日がたつのは早いもので今年も残すところ後9日です。
前回のブログ更新直後にtwitterで「あっ」とつぶやいてみれば、それが公開される頃には一年以上の月日が過ぎていました。
夏にはカブトムシが群がる程ワキの甘い一年でしたが、最期ぐらいはビシッと締める必要があります。
そんな僕にDesign by Contract(以下 DbC)です。
javaにはDbCをサポートするツールとしてContract4Jなどがありますが、
今回はscala wikiに掲載されていた、traitを使用したシンプルなDbCの実現方法の紹介とコードの解説をしたいと思います。
そもそもDbCってなによって方はまずこの辺りを参照してください。
まず初めに、契約を記述する為のContracted traitを以下のように定義します。
trait Contracted { class AssertionFailed extends Error type Conds = List[() => Boolean] protected case class Contract(reqs:Conds, enss:Conds) { def require(test: => Boolean) = Contract((() => test)::reqs, enss) def ensure(test: => Boolean) = Contract(reqs,(() => test):: enss) def in[T](body: => T):T = { for(r <- reqs.reverse if(!r())) throw new AssertionFailed() val ret = body for(e <- enss.reverse if(!e())) throw new AssertionFailed() ret } } def require(test: => Boolean) = Contract((() => test) :: Nil, Nil) def ensure(test: => Boolean) = Contract(Nil, (() => test) :: Nil) }
Contractedに定義されたrequireとensureが事前条件と事後条件を定義する為のメソッドです。
この二つのメソッドは、「引数0でBooleanを返すの関数」を引数として指定していますが、
こうすることで内部に書かれた式を関数として受けとり、後から遅延評価させることができます。
requireのensureの返り値の型はContractedの内部クラスとして定義されている
Contractですが、このクラスの説明をする前に実際の使用方法をみてみましょう。
CbDを行なうには、対象のクラスに先程のContractedをmixinし、
更にrequireとensureで事前条件と事後条件を定義した後、inメソッドに実際のロジックを記述します。
class Account(b: Int) extends Contracted { def withdraw(amount: Int) { val old_balance = balance ( require(amount > 0) require(balance - amount >= 0) ensure(old_balance - amount == balance) ) in { balance -= amount } } } val account = new Account(1000) account.withdraw(300) account.withdraw(800) // 事前条件 「balance - amount >= 0」をみたさないのでエラー
事前条件と事後条件の書き方は、DSL的な特殊な構文に見えますが、
これは先程のContractクラスとドットの省略を組み合わせるテクニックで実現しています。
それを理解する為に、試しにドットなどを省略せずに冗長な記述してみます。
するとこれは単純にメソッドチェインをしているだけということが判るかと思います。
val contract1 = this.require(amount > 0) val contract2 = contract1.require(balance - amount >= 0) val contract3 = contract2.ensure(old_balance - amount == balance) contract3.in { balance -= amount }
最期に先程説明を省略したContractの実装をみてみましょう。
protected case class Contract(reqs:Conds, enss:Conds) { def require(test: => Boolean) = Contract((() => test)::reqs, enss) def ensure(test: => Boolean) = Contract(reqs,(() => test):: enss) def in[T](body: => T):T = { for(r <- reqs.reverse if(!r())) throw new AssertionFailed() val ret = body for(e <- enss.reverse if(!e())) throw new AssertionFailed() ret } }
requireやensureが呼ばれるとのリスト先頭に新しい契約を追加した、新しいContractを返します。
これまでに定義した契約が守られているかをbodyの前後のfor文で確認しているわけです。
さて次はScalaやってて知らない奴はモグリといっても過言ではない、
どうみてもチン○のマスコットでお馴染の@yuroyoroさんです。