Tambourine作業メモ

主にスキル習得のためにやった作業のメモ。他人には基本的に無用のものです。

ケント・ベックの「テスト駆動開発」の写経をRustでやってみる(11)

どうやらほとんど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章はこれで終わり。