13章「実装を導くテスト」に入る。
最終的にどんなコードを目指しているのか見失ったままストーリーが続いているのだが (いや、一度、この部分は読んでいるんだけど・・・これが写経の効果だな)、 とりあえず、著者のインサイトに従っていくことにする。
さて、addメソッド(本ではplusメソッド)の戻りはSumというクラスでなくては ならないらしい。なるほど・・・Expressionは演算する式をイメージしているのに、 演算結果をMoneyで返していてはイカンということなんだな。
本ではplusメソッドの戻りであるresultがSumのインスタンスであることを キャストするコードをテストに入れる事によって強制している。
Sum sum = (Sum) result;
ちゃんとSumクラスを実装すれば、これはJavaでは実行時エラーになる。
では、Rustではどうだろうか。
単純に、Javaのコードを書き写せばこうなるんだろうか。
let result: Box<Expression> = five.add(&five); let sum: Box<Sum> = result as Box<Sum>;
Sum構造体の定義を追加しさえすれば、 Javaならこれでコンパイルは通るんだけど・・・
error[E0605]: non-primitive cast: `std::boxed::Box<dyn Expression>` as `std::boxed::Box<Sum>` --> src/main.rs:99:29 | 99 | let sum: Box<Sum> = result as Box<Sum>; | ^^^^^^^^^^^^^^^^^^ | = note: an `as` expression can only be used to convert between primitive types. Consider using the `From` trait
実行時エラーになるようなものは、そもそもコンパイルが通らないのがRustである。
エラーメッセージでは、「asで型変換したけりゃFromトレイトを実装しろ」と言っている。Fromトレイトはある型から別の型を作る(例えば、4つの整数の組や文字列からIPv4アドレスを作るとか)ための仕組みで、そーゆーのもあるにはあるんだけど、今、ここでやりたいのはポリモーフィズムとかダックタイプなので、Fromトレイトを使うのは適切ではない。
ちなみに、実行時にメソッドバインディングするという意味でのポリモーフィズムは、今まさにやっているようにBoxでトレイトの参照を持つこと(トレイトオブジェクトという)で可能だ。もちろん、Rustにとってそれはベストなやり方ではない(静的にバインドされてる方が性能的に有利だし、そういう方向性がRustの指向するところだ)。
というわけで、キャストする方向は止めておこう。普通にSumオブジェクトを使ったテストにしておく。addメソッドはSumを返す。Sumはメンバーとして足す数と足される数を持っている。
さて、Sumはどういうものなのだろうか。次に本で作られたテストはこういうモノだ。
@Test public void testPlusReturnsSum() { Money five = Money.dollar(5); Expresson result = five.plus(five); Sum sum = (Sum) result; assertEquals(five, sum.augent); assertEquals(five, sum.addend); }
Sumにはaugent(足される数)とaddend(足す数)という2つのMeney型のフィールドがあるようだ。Moneyのplusメソッドは、Sumのインスタンスを作り、自分自身をaugentに、引数をaddendにセットして返す。
ただし、Rustで単にSumにMoneyの参照を保持するようにするのは違うような気がする。値の演算を保持することを考えると、渡されたMoneyの複製を保持するべきだろう。渡されるMoneyがイミュータブルであるとは限らないし。 上のJava版のテストのRust版はこうした。
#[test] fn test_add_returns_sum() { let five = Money::dollar(5); let sum = five.add(&five); assert_eq!(five, sum.augend); assert_eq!(five, sum.addend); }
対応するaddの実装はこうなる。amountはi32なので、複製が作られる。currencyはどれも同じスライスを指している。
pub fn add(&self, other: &Money) -> Box<Sum> { Box::new(Sum { augend: Money { amount: self.amount, currency: self.currency, }, addend: Money { amount: other.amount, currency: other.currency, }, }) }
まあ、シンプルだ。しかし、Moneyの複製を作るコードが重複しているのは良くない。ここで、本の流れから逸れて、複製のためのメソッドを作ってしまおう。
と言っても、例によって何も考えずにフィールドを複製すれば複製が作られるようなものなら、Rustが勝手にやってくれる。Cloneトレイトを使う。デフォルト実装で避ければ、#[derive(Clone)]
をクラスに付ければいいだけだ。
#[derive(Debug, Eq, PartialEq, Clone)] pub struct Money { amount: i32, currency: &'static str, } impl Money { (略) pub fn add(&self, other: &Money) -> Box<Sum> { Box::new(Sum { augend: self.clone(), addend: other.clone(), }) } }
もう一歩進んでCopyトレイトの事も考えたいが、それは後に置いておくことにしよう。
次は、reduceメソッドだ。今のreduceメソッドは何を入れても10ドルを返すダミーメソッドだが、これをちゃんとしよう。まず、ちゃんとしていないとエラーになるようなテストを書く。
#[test] fn test_reduce_sum() { let sum = Box::new(Sum { augend: Money::dollar(3), addend: Money::dollar(4), }); let result = bank::reduce(sum, "USD"); assert_eq!(Money::dollar(7), result); }
テストがfailすることを確認したら、実装に移ろう。
さて、bank::reduceメソッドのシグネチャは
pub fn reduce(source: Box<Expression>, to: &str) -> Money
である。
ここでExpressionはImposterパターンを使うために導入されたことを 思いだそう。Imposter(偽物)パターンは、まとめて扱いたいものを あたかも同一種のように扱うためのパターンである。 つまり、Expressionトレイトを実装していれば、 それがどんなモノでもreduceが行えるような仕組みにする必要がある。 というわけで、Expressionトレイトにreduceというメソッドを作り、 渡ってきたオブジェクトに処理を委譲する。 Sumのreduceの実装では、そこに足し算の処理も入れてしまえばいい。
やっとやりたいことが見えてきた。なるほど。
というわけで、Expressionトレイトはこうなる。
pub trait Expression { fn reduce(&self, to: &str) -> Money; }
MoneyもExpressionトレイトを実装しているので reduceメソッドを実装しなければならない。 まあ、Moneyを返せば良いので自分を複製して返すことにする。 本当はここで通貨の両替が実装されるのである。 いずれテストが書かれ、クソ実装が暴かれるだろう。
impl Expression for Money { fn reduce(&self, to: &str) -> Money { self.clone() } }
Sumの実装も、両替はほったらかし。とりあえず3+4が計算できるようにして、テストを通そう。
impl Expression for Sum { fn reduce(&self, to: &str) -> Money { Money { amount: self.augend.amount + self.addend.amount, currency: to, } } }
これでばっちり・・・とならないところが残念。コンパイルエラーである。
error[E0312]: lifetime of reference outlives lifetime of borrowed content... --> src/main.rs:74:23 | 74 | currency: to, | ^^ | = note: ...the reference is valid for the static lifetime... note: ...but the borrowed content is only valid for the anonymous lifetime #2 defined on the method body at 71:5 --> src/main.rs:71:5 | 71 | / fn reduce(&self, to: &str) -> Money { 72 | | Money { 73 | | amount: self.augend.amount + self.addend.amount, 74 | | currency: to, 75 | | } 76 | | } | |_____^
ライフタイムである。reduceに渡ってきた文字列スライスtoが、戻り値に入っているけど、このtoがいつまで有効かわからんからヌルポになる可能性があるよ、といわれているわけだ。そもそも、Moneyのcurrencyに入れるのは'static
な参照だけのつもりなので、それしか入れられないように引数に指定をしてしまえばいい。
こんな感じだ。
pub trait Expression { fn reduce(&self, to: &'static str) -> Money; }
これでテストは全て通る。両替は次の章で実装されるってことで、今回はここまで。