Tambourine作業メモ

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

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

9章に入る。いよいよ、通貨の導入だ。

その前に、Dollar構造体とFranc構造体の統一を片付けてしまおう。 9章の展開とは違うんだけど、とりあえず、以下の様なテストが通ることを目標にする。

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_multiplication() {
        let five_d = Money::dollar(5);
        assert_eq!(Money::dollar(10), five_d.times(2));
        assert_eq!(Money::dollar(15), five_d.times(3));

        let five_f = Money::franc(7);
        assert_eq!(Money::franc(35), five_f.times(5));
        assert_eq!(Money::franc(77), five_f.times(11));
    }

    #[test]
    fn test_equality() {
        assert_eq!(Money::dollar(5), Money::dollar(5));
        assert_ne!(Money::dollar(5), Money::dollar(6));
        assert_eq!(Money::franc(7), Money::franc(7));
        assert_ne!(Money::franc(7), Money::franc(6));

        // 通貨が違うので、一致しない。
        assert_ne!(Money::franc(5), Money::dollar(5));
    }
}

ここにはDollarもFrancも影も形もない。

コードは今はこんな感じ。

trait Money {
    fn new(i: i32) -> Self;

    // Factory Method
    fn dollar(i: i32) -> Dollar {
        Dollar::new(i)
    }
    fn franc(i: i32) -> Franc {
        Franc::new(i)
    }

    fn amount(&self) -> i32;
    fn times(&self, multiplier: i32) -> Self
    where
        Self: Sized,
    {
        Self::new(self.amount() * multiplier)
    }
}

#[derive(Debug, Eq, PartialEq)]
pub struct Dollar {
    amount: i32,
}

impl Money for Dollar {
    fn new(i: i32) -> Dollar {
        Dollar { amount: i }
    }

    fn amount(&self) -> i32 {
        self.amount
    }
}

impl PartialEq<Franc> for Dollar {
    fn eq(&self, _other: &Franc) -> bool {
        false
    }
}

#[derive(Debug, Eq, PartialEq)]
pub struct Franc {
    amount: i32,
}

impl Money for Franc {
    fn new(i: i32) -> Franc {
        Franc { amount: i }
    }
    fn amount(&self) -> i32 {
        self.amount
    }
}

impl PartialEq<Dollar> for Franc {
    fn eq(&self, _other: &Dollar) -> bool {
        false
    }
}

もちろんコンパイルエラーになるはずだ。まずは、エラーの解消を目指そう。

DollarとFrancにある処理をMoney構造体に集める。 ほぼ機械的にできるし、とてもコードが短くなった。

#[derive(Debug, Eq, PartialEq)]
pub struct Money {
    amount: i32,
}

impl Money {
    fn dollar(i: i32) -> Money {
        Money { amount: i }
    }

    fn franc(i: i32) -> Money {
        Money { amount: i }
    }

    fn amount(&self) -> i32 {
        self.amount
    }

    fn times(&self, multiplier: i32) -> Money {
        Money {
            amount: self.amount() * multiplier,
        }
    }
}

テストを実行すると、ドルとフランを比較するところでテストに失敗する。 以前は型が違う場合はエラーにしていたんだから、それはそうだ。

というわけで、いよいよここで通貨の導入だ。 本と同じように、文字列で通貨を表現することにしてみよう。

pub struct Money {
    amount: i32,
    currency: str,
}

しかし、この変更はコンパイルエラーになる。 strはいわゆるC言語の文字列みたいなものなので、サイズが決まらないからだ。

error[E0277]: the size for values of type `str` cannot be known at compilation time
  --> src/main.rs:11:26
   |
11 |     fn dollar(i: i32) -> Money {
   |                          ^^^^^ doesn't have a size known at compile-time
   |
   = help: within `Money`, the trait `std::marker::Sized` is not implemented for `str`
   = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
   = note: required because it appears within the type `Money`
   = note: the return type of a function must have a statically known size

そもそも、C言語に倣うならMoneyが持っているのは文字列へのポインタになるはずだ。 というわけで、strへの参照に変えてみる。

pub struct Money {
    amount: i32,
    currency: &str,
}

しかし、これもダメだ。

error[E0106]: missing lifetime specifier
 --> src/main.rs:7:15
  |
7 |     currency: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
5 | pub struct Money<'lifetime> {
6 |     amount: i32,
7 |     currency: &'lifetime str,
  |

currencyが指している先の文字列がずっとある保証がないからだ。 エラーメッセージでは、ライフタイムパラメタを入れたらどうかと言われている。

入れてみよう。

#[derive(Debug, Eq, PartialEq)]
pub struct Money {
    amount: i32,
    currency: &'static str,
}

impl Money {
    fn dollar(i: i32) -> Money {
        Money {
            amount: i,
            currency: "USD",
        }
    }

    fn franc(i: i32) -> Money {
        Money {
            amount: i,
            currency: "CHF",
        }
    }

    fn amount(&self) -> i32 {
        self.amount
    }

    fn times(&self, multiplier: i32) -> Money {
        Money {
            amount: self.amount() * multiplier,
            currency: self.currency,
        }
    }
}

'statucは、プログラムの起動中はずっと存在することを示すライフタイムだ。 このコードではcurrencyに入る可能性があるのは、文字列リテラルだけなのでこれでOK。

では、eqメソッドを実装しよう。ついでに、全部のメソッドにpubを付ける。 付けていないと、「使われていないよ」というワーニングが出るのがありがたい。

pub fn eq(&self, other: &Money) -> bool {
      if self.currency != other.currency {
          false
      } else {
          self.amount == other.amount
      }
}

これでテストも全部通過した。素晴らしい。