C# テスト駆動開発の実践方法:オブジェクトを改修してテストする #9

前回の記事「C# テスト駆動開発の実践方法:バリューオブジェクトを考える #8」ではバリューオブジェクトの生成までを作ってみました。バリューオブジェクトは意味のあるデータの単位をオブジェクトとして表現して「重要な概念を表現する」ために使用できます。

前回は単純にお金を表すバリューオブジェクトを作成するところまでを実施しましたが、今回は今後の展開を見据えてMoneyクラスに処理を追加していきたいと思います。

バリューオブジェクトを作る一つの利点として挙げられるのは「処理をまとめることができる」という点になります。Moneyクラスに対して何かをする処理を、わざわざUtilクラスなどに作成していくと、同じような処理が沢山作成されてしまう可能性があります。今回はここを深堀していきます。

バリューオブジェクトに処理を加える

さて、この連載で最終的に作成したいアプリケーションは「口座」ののような特性を持ちますが、「口座」からの処理として出金と入金があると伝えていました。これは処理の中身を想像してもすぐに分かる通り「お金」に対して追加したり、削減したりする処理が想像できるはずです。これらの処理をMoneyクラスに実装してしまおうということです。

足し算をするメソッドを実装する

それではまず最初にテストクラスに移動して、新規のテストメソッドを作成してみます。テストメソッドを作成するには「public void」のメソッドを作成して属性として[TestMethod]という属性を付与することになります。以下を新しくメソッドとして追加してください。

[TestMethod]
public void AddMoneyTest()
{
    var money = new Money(100);
    int added = money.Add(100);
    Assert.AreEqual(added, 200);
}

さてまずは汚くても通るコードを考えてみます。例えば元々100のお金に対して、さらに100を追加するような場面を考えています。とりあえずAddメソッドを作ると考えて、そこに100を渡すようにします。

100と100を足すと200になりますので、いったんはint型でそのまま200と返すようにします。まだこの段階ではリファクタリングはせず、とりあえず形にしたい概念をダイレクトに表現してみます。当然のごとくコンパイルエラーになりますので、Moneyクラスに処理が通るようにAddメソッドを追加してみましょう。Moneyクラスは以下のようになるはずです。

namespace App03.ValueObjects
{
    public class Money
    {
        public int Value { get; private set; }

        public Money(int value)
        {
            this.Value = value;
        }

        public int Add(int add)
        {
            return this.Value + add;
        }
    }
}

Addメソッドを見てみると追加した引数に内部で保留している値を足し算して返すようにしました。かなり簡単なやり方で、そんなに綺麗とも言えないとパッと見ても思う形です。とりあえず形を実現するための土台としてはOKです。テストを実施してグリーンになれば次に進めます。

洗練されたオブジェクトの足し算にする

先ほどの処理はグリーンにしましたが、そこまできれいな形であったとは言えませんでした。もう少し「意味のある形」でMoneyクラスの足し算を行えるようにリファクタリングしたいですね。

「足し算をする」の前提を考えてみましょう。足し算をするための前提は、足し合わせるオブジェクト同士が同一の形であることが望ましいと言えます。1+1が2になるのも、1が数値同士であるから足し合わせることができるという前提です。

そこを今回のMoneyに当てはめると、Moneyというオブジェクトを足し合わせるにはMoneyのオブジェクトである必要があります。今回作成したメソッドは引数に数値を直接的に渡していました。Moneyオブジェクトの足し算をするのに数値を渡すのは不自然です。なので目的の足し算を少し修正したいと思います。まずはテストクラスの先ほどのテストメソッドを理想的な形式にリファクタリングします。

[TestMethod]
public void AddMoneyTest()
{
    var money = new Money(100);
    var target = new Money(100);
    int added = money.Add(target);
    Assert.AreEqual(added, 200);
}

こんな感じに修正してみました。MoneyクラスのAddメソッドの引数にMoneyクラスのオブジェクトを要求することで、より正しい「足し算の形」に近づけたのではないかと思います。いったんはこの形式でテストをグリーンな状況に持っていきます。Moneyクラスを以下のように修正しました。

namespace App03.ValueObjects
{
    public class Money
    {
        public int Value { get; private set; }

        public Money(int value)
        {
            this.Value = value;
        }

        public int Add(Money target)
        {
            return this.Value + target.Value;
        }
    }
}

Addメソッドの引数を見てみてください。Moneyクラスのオブジェクトを要求して、内部で保管するValueと足し算をして返却する様にしています。いったんグリーンになるか確認をして、想定通りグリーンになっていれば、ここまでのリファクタリングは問題ないことが証明できます。

汎用性のあるオブジェクト

さて次に目を付けたのはテストメソッド内の3行目の記述の部分になります。以下の箇所に不自然さを覚えた人も多いかもしれませんね。

int added = money.Add(target);

足し算をしたらint型の値で返ってきているということです。そのあとのAreEqualの処理で返却値が想定通りであることは問題ないのですが、Moneyクラスの足し算をしたらint型の変数が返ってくるのも不自然ですよね。こういう場合は2つの選択肢が予想できるかと思います。

1. void型の変数にしてオブジェクトの値自身を変えてしまう
2. 新しいオブジェクトを生成して返却する

まずは1の案で進んでみたいと思います。void型の変数にしてオブジェクトの値を変えてしまうという戦略です。これを実施するためにテストを以下のように書き換えてみます。

[TestMethod]
public void AddMoneyTest()
{
    var money = new Money(100);
    var target = new Money(100);
    money.Add(target);
    Assert.AreEqual(money.Value, 200);
}

どうでしょうか。足し合わされる元になるオブジェクトに対して足し算をすることで、内部の格納値を変更するような記載となっています。いったんはこれをグリーンの状態にするために修正を加えます。Moneyクラスは以下のような感じになると思います。

namespace App03.ValueObjects
{
    public class Money
    {
        public int Value { get; private set; }

        public Money(int value)
        {
            this.Value = value;
        }

        public void Add(Money target)
        {
            this.Value += target.Value;
        }
    }
}

とりあえずテストを実施してグリーンになることを確認します。これでグリーンになっていれば、リファクタリングでコードを壊してないことが証明されます。「この形式で問題ないか?」を考えてみますが、ちょっと汎用性が低いことに気が付きました。連続で足し算ができない、ということです。

money.Add( ... ).Add( ... );

いまの処理の形式では上記のように処理をチェーンして書けないことに気が付きます。「口座」の入出金に対応するだけなら、別にいいような感じがしますが、あくまでもMoneyクラスは「お金」の概念を表すので、連続して足し引きができてもいいような気がします。というわけでテストを書き直してみます。

[TestMethod]
public void AddMoneyTest()
{
    var money = new Money(100);
    var newMoney = money.Add(new Money(60)).Add(new Money(50));
    Assert.AreEqual(newMoney.Value, 210);
}

こんな感じでAddを2回連続でできるようにしました。これを満たすためにはAddの戻り値をMoneyクラスにする必要がありますね。ということでMoneyクラスの中身を以下のように修正します。

namespace App03.ValueObjects
{
    public class Money
    {
        public int Value { get; private set; }

        public Money(int value)
        {
            this.Value = value;
        }

        public Money Add(Money target)
        {
            var newValue = this.Value + target.Value;
            return new Money(newValue);
        }
    }
}

元々のオブジェクトを破棄して、新規のMoneyオブジェクトを返却する様にしています。こうすることで元のオブジェクトに対して影響を与えないオブジェクトを生成することができます。それぞれのオブジェクトが互いに影響しない状態であると言えます。これを「イミュータブルなオブジェクト」といいます。詳しくは「C# オブジェクト指向の基礎:コンストラクタを活用する方法 #7」で紹介しています。

またMoneyクラスのAddメソッドの方針としてオブジェクトが知らぬところで干渉しないようにしたので、Moneyクラス内のプロパティの持ち方も完全コンストラクタパターンに合わせるようにします。要するにコンストラクタでしか値を設定できなくするということです。

このように既存の処理に対して微調整を行う場合は、テストメソッドに変更をすることなく、ソースコードのみを変更してテストを再実施してグリーンな状態にすればOKです。Moneyクラスを以下のように編集しましょう。

namespace App03.ValueObjects
{
    public class Money
    {
        public int Value { get; }

        public Money(int value)
        {
            this.Value = value;
        }

        public Money Add(Money target)
        {
            var newValue = this.Value + target.Value;
            return new Money(newValue);
        }
    }
}

ValueプロパティのSetterを削除しました。プロパティに対してgetterのみ存在する場合はコンストラクタでのみ値を設定することが可能です。もちろんテストはグリーンの状態なので元のコードは破壊することなくリファクタリングが完了していることになります。

テスト駆動開発は続く

今回はここまでとします。次回は足し算の反対である引き算をするメソッドをテスト駆動開発形式で取り組んでいきます。Addメソッドをテスト駆動開発に沿って取り組んできたので、引き算はそこまで難しくはないと思います。

今回の記事ではテストを実施しながらリファクタリングを行っていくという、本来あるべき手法のテスト駆動開発を実践することができました。これまでメリットを余り感じることができなかった人も「なるほど!」と思ってもらえたのではないかと思います。

というわけで次回は今回の復習に近い内容となりますが、Moneyクラスに引き算をするメソッドを実装していきましょう。