どうやらほとんど11章まで終わっていたみたいなので、12章に入る。
やりたいことはドルとフランの間で透過的に演算が出来ることだ。 「$5 + 10 CHF = $10 (レートが2:1の場合)」みたいなことがしたい。
ただし、まずやるべきは同じ通貨同士の足し算だ。
というわけで、テストを書いた。
#[test] fn test_simple_addition() { let sum = Money::dollar(5).add(Money::dollar(5)); assert_eq!(Money::dollar(10), sum); }
本ではメソッド名はplusになっているが、addにした。
というのも、Rustには演算子オーバーロードの機能があり、addを実装しておけば+
で足し算が出来るようになりそうだから。
それも試してみよう。
もちろんまだテストは実行できない。メソッド定義してないし。
Compiling money v0.1.0 (/Users/tambara/study/tdd_rust/money) error[E0599]: no method named `add` found for struct `Money` in the current scope --> src/main.rs:75:36 | 5 | pub struct Money { | ---------------- method `add` not found for this ... 75 | let sum = Money::dollar(5).add(Money::dollar(5)); | ^^^ method not found in `Money` | = help: items from traits can only be used if the trait is implemented and in scope = note: the following trait defines an item `add`, perhaps you need to implement it: candidate #1: `std::ops::Add`
ただ、「お前、もしやstd::ops::Add
トレイトが実装したいのでは?」と
言ってきてる。エスパーかな?
このトレイトを実装すると、+
が使えるようになるはずなワケ。
とりあえず、そこは置いておいて、テストを通すことにする。
pub fn add(&self, other: &Money) -> Money { Money { amount: self.amount + other.amount, currency: self.currency, } }
酷いコードだけど、まずはこれでOK。
書いてみてわかったのは、引数には参照を貰うべきだね。 なので、テストも参照を渡すように変更。
let sum = Money::dollar(5).add(&Money::dollar(5));
これで問題ない。
さて、本ではこの後、通貨の換算をするのにImposterパターンを使うと言い出した。 恥ずかしながら初めて聞いた。ググってみたが、みんなあんまりよく知らない感じがする(笑)。
説明も読んでみたんだけど、ExpressionインターフェースとBankクラスがどういう働きをするのか、 12章の段階ではよくわからない。 Expressionインターフェースにはまだ何のメソッドもないし、Bankクラスはデータを持たずに ただreduceというメソッドを持つだけだ。
そこで、Expressionトレイトとbankモジュールという形で実装してみることにしよう。
まず、Java版のテストはこんな感じだ。
@Test public void testSimpleAddition() { Money five = Money.dollar(5); Expression sum = five.plus(five); Bank bank = new Bank(); Money reduced = bank.reduce(sum, "USD"); assertEquals(Money.dollar(10), reduced); }
素直にRustにしてみよう。
#[test] fn test_simple_addition() { let five = Money::dollar(5); let sum: Expression = five.add(&five); let reduced: Money = bank::reduce(sum, "USD"); assert_eq!(Money::dollar(10), reduced); }
これはダメだ。Expressionはトレイトなのでsumのサイズがコンパイル時に決まらない。 参照にしてみよう。すると、エラーはなくなる。
しかし、addメソッド側で困ってしまう。
pub fn add(&self, other: &Money) -> &Expression { let m = Money { amount: self.amount + other.amount, currency: self.currency, }; &m }
これはエラーになる。
error[E0515]: cannot return reference to local variable `m` --> src/main.rs:49:9 | 49 | &m | ^^ returns a reference to data owned by the current function
メソッドの中で作ったものを外に持ち出すのなら、移動させなければならない。 参照を返しても、参照が指しているオブジェクトを移動させないのならば、 メソッドが終わったらそのオブジェクトは無くなってしまう。
これをどうするのがRustらしいのかというのはあんまりよくわからないんだけど、 C言語的な発想ではmallocすることになるわけだ。
Rustではそういう場合、Boxという賢いポインタを使うことになる。 こんな良くないコードになるのはそもそもRustにマッチしていない設計を 選んでしまっているからじゃないかという疑いもあるけど。
pub fn add(&self, other: &Money) -> Box<Expression> { Box::new(Money { amount: self.amount + other.amount, currency: self.currency, }) }
このように作ったMoneyオブジェクトを、Boxに詰めて送り出す。 受ける方は、これでOK。
let sum: Box<Expression> = five.add(&five);
とりあえず、12章はこれで終わり。