Tambourine作業メモ

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

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

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;
}

これでテストは全て通る。両替は次の章で実装されるってことで、今回はここまで。