Tambourine作業メモ

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

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

さて、ここでちょっと立ち止まって、コードを眺めてみよう。 コード全体は今でも100行ちょっとに過ぎない。

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

impl Money {
    pub fn dollar(f: f64) -> Money {
        Money {
            amount: f,
            currency: "USD",
        }
    }

    pub fn franc(f: f64) -> Money {
        Money {
            amount: f,
            currency: "CHF",
        }
    }

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

    pub fn add(&self, other: &Money) -> Box<Sum> {
        Box::new(Sum {
            augend: self.clone(),
            addend: other.clone(),
        })
    }
}

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

impl Expression for Money {
    fn reduce(&self, to: &'static str, bank: &Bank) -> Result<Money, &'static str> {
        let rate = bank.get_rate(self.currency, to);
        match rate {
            Some(r) => Ok(Money {
                amount: self.amount / r,
                currency: to,
            }),
            None => Err("Never set rate these currencies"),
        }
    }
}

pub struct Sum {
    augend: Money,
    addend: Money,
}
impl Expression for Sum {
    fn reduce(&self, to: &'static str, _: &Bank) -> Result<Money, &'static str> {
        Ok(Money {
            amount: self.augend.amount + self.addend.amount,
            currency: to,
        })
    }
}

pub struct Bank {
    rate: HashMap<(&'static str, &'static str), f64>,
}

impl Bank {
    pub fn new() -> Bank {
        Bank {
            rate: HashMap::new(),
        }
    }

    fn get_pair(one: &'static str, another: &'static str) -> (&'static str, &'static str) {
        if one.to_string() < another.to_string() {
            (one, another)
        } else {
            (another, one)
        }
    }
    pub fn add_rate(&mut self, from: &'static str, to: &'static str, r: f64) {
        let pair = Bank::get_pair(from, to);
        self.rate
            .insert(pair, if pair.0 == from { r } else { 1.0 / r });
    }

    pub fn get_rate(&self, from: &'static str, to: &'static str) -> Option<f64> {
        if from == to {
            return Some(1.);
        }

        let pair = Bank::get_pair(from, to);
        self.rate
            .get(&pair)
            .map(|i| if pair.0 == from { *i } else { 1.0 / *i })
    }

    pub fn reduce(&self, source: Box<Expression>, to: &'static str) -> Result<Money, &str> {
        source.reduce(to, self)
    }
}

まず、違和感があるのはMoney.sumだ。

pub fn add(&self, other: &Money) -> Box<Sum>

timesがMoneyを新しく作ってそれをそのまま返しているのに、addはSumのポインタを返している。これはもともとExpressionに汎用化してどーたらこーたらやっている名残でこうなっているわけで、別にSumをそのまま返せないことはない。

なので、シンプルに

pub fn add(&self, other: &Money) -> Sum {
    Sum {
        augend: self.clone(),
        addend: other.clone(),
    }
}

これで良いはずだ。

このSumはBank.reduceに渡されていたワケだが、そっちは

pub fn reduce(&self, source: Box<Expression>, to: &'static str) -> Result<Money, &str>

というシグネチャになっている。ここでもExpressionを実装した型の参照を受け取りたいが、&Epressionとは書くことができないので、トレイトオブジェクトを渡している。

これもJavaのコードに引きずられた形だ。このケースの場合、このようなトレイトオブジェクトを使うのでは無く、ジェネリクスで書くのがRustのスタイルのようだ。このようにポリモーフィズムを表現するのにRustではジェネリクスかトレイトオブジェクトかを選択して使うことになる・・・らしい。どちらも、JavaC#ではおなじみに使う構文によく似ているのだが、この2つを並列に並べて、「どっちが使おうか」なんてあんまり考えたことがないので、ちょっと戸惑う。

ともかく、ここで無駄にポインタを使うのではなく、ジェネリクスを使うと随分スッキリしたイメージになる。こうだ。

pub fn reduce<T: Expression>(&self, source: &T, to: &'static str) -> Result<Money, &str>

これで、sourceはExpressionを実装した型の参照が来るよ、という意味になる。しかし、ライフタイムのパラメータと型パラメータをどの位置に書けば良いのか覚えられない。慣れなのかな・・・。

これでちょっとキレイになって満足だ。