😺

依存は注入して快適なテストライフを送ろう

2022/01/29に公開

依存性の注入という言葉を頻繁に聞いていたがいまいちしっくりきていなかったので、自分なりにまとめた。

依存生の注入って何???

天下のwiki様によるとこうだ

依存性の注入(いそんせいのちゅうにゅう、英: Dependency injection)とは、コンポーネント間の依存関>>係をプログラムのソースコードから排除するために、外部の設定ファイルなどでオブジェクトを注入できるように>するソフトウェアパターンである

依存に関しての設計パターンなのだろうがどうもしっくりこない。
ということで理解を深めるためにジャンケンプログラムを作ってみた(唐突

注入前のコード

Playerくんはグーチョキパーをランダムに出すことが出来、Refereeが結果を判定する。

zenn.java
class Main{
    public static void main(String[] args){
        Referee referee = new Referee();
        System.out.println(referee.displayResult());
    }
}

class Referee{
    String displayWinner(){
        Player player1 = new Player();
        Player player2 = new Player();

        int player1Hand = player1.hand();
        int player2Hand = player2.hand();

        if(player1が勝っていたら){
            return "winner is player1";
        }else{
            return "winner is player2";
        }
    }
}

class Player{
    public int hand(){
        //ランダムでグーかチョキかパーを返す
    }
}

仕様もそこまで複雑じゃないし、まあ大丈夫だろう。

.
.
.

本当に???

テストをしてみよう

上記のコードのReferee#displayWinnerをテストをしてみよう。
正しく勝利判定をしているかどうか。

、、、出来なくない!???

テストケースでよくあるのは組み合わせのテストだ。
今回の場合だとグーとチョキ、パーとグーと言った具合だ。

しかし、上記のコードだと何を出すかはランダムで決まるためテストが非常にしにくい。

注入しよう!依存性を

そこで以下のようにコードを変えてみた

class Main{
    public static void main(String[] args){
        Referee referee = new Referee();
	
        RockPaperScissors player1 = new Player();
        RockPaperScissors player2 = new Player();
	
        System.out.println(referee.displayWinner(player1, player2));
    }
}

class Referee{
    String displayWinner(RockPaperScissors player1, RockPaperScissors player2){
        int player1Hand = player1.hand();
        int player2Hand = player2.hand();

        if(player1が勝っていたら){
            return "winner is player1";
        }else{
            return "winner is player2";
        }
    }
}

interface RockPaperScissors{
    int hand();
}

class Player implements RockPaperScissors{
    public int hand(){
        //グーかチョキかパーを返す
    }
}

displayWinner内でプレイヤーを作るのでなく、引数として渡されるようにした。
また引数で渡せるのはプレイヤーだけでなくRockPaperScissorsインターフェースを実装しているクラスなら何でもありになった。
これで以下のようにテストが出来るようになった。

zenn.java
class Main{
    public static void main(String[] args){
        Referee referee = new Referee();
	
        RockPaperScissors rock = new MockRock();
        RockPaperScissors scissors = new MockScissors();
        RockPaperScissors paper = new MockPaper();
	
        //グーが勝つことを確認
        System.out.println(referee.displayWinner(rock, scissors));
	
        //チョキが勝つことを確認
        System.out.println(referee.displayWinner(paper, scissors));
	
        //パーが勝つことを確認
        System.out.println(referee.displayWinner(rock, paper));
    }
}

class Referee{
    String displayWinner(RockPaperScissors player1, RockPaperScissors player2){
        int player1Hand = player1.hand();
        int player2Hand = player2.hand();


        if(player1が勝っていたら){
            return "winner is player1";
        }else{
            return "winner is player2";
        }
    }
}

interface RockPaperScissors{
    int hand();
}

class MockRock implements RockPaperScissors{
    public int hand(){
            //絶対グーを返す
        return ROCK;
    }
}

class MockScissors implements RockPaperScissors{
    public int hand(){
        //絶対チョキを返す
        return SCISSORS;
    }
}

class MockPaper implements RockPaperScissors{
    public int hand(){
        //絶対パーを返す
        return PAPER;
    }
}

上記のように引数にオブジェクトを渡すように変えるとMockRock,MockScissors,MockPaperなどの偽物のデータいわゆるモックを差し込むことが可能になりテストが容易になります。

え、これじゃhandメソッドのテストが出来てなくない?と思われた方、良いんです。Refereeが行うのはジャンケンの結果判定のみで誰が何を出そうかなんて知ったことではありません。

おまけ

上記のコードでジャンケンできるクラス全てにRockPaperScissorsをimplementsさせたのはテストをしやすくする他にもう一つ理由があります。
それは実装を抽象化させるためです。
じゃんけんと言うのは人それぞれ出す手が違います。
グーをたくさん出す人もいたり、パーだけ出す人もいます。
しかし、出す手は違えどみんなやっていることは同じジャンケンです。

zenn.java
class Main{
    public static void main(String[] args){
        Referee referee = new Referee();

        RockPaperScissors rockPlayer = new RockPlayer();
        RockPaperScissors paperPlayer = new PaperPlayer();
	
        System.out.println(referee.displayWinner(rockPlayer, paperPlayer));
    }
}

class Referee{
    String displayWinner(RockPaperScissors player1, RockPaperScissors player2){
        //勝利判定メソッド
    }
}

class RockPlayer implements RockPaperScissors{
    public int hand(){
        //グーをたくさん出すメソッド
    }
}

class PaperPlayer implements RockPaperScissors{
    public int hand(){
        //パーしか出さないメソッド
    }
}

インターフェースに対してプログラムをすることで対象のメソッドを実装していればどんなクラスでも引数で渡してきて良いよ〜とクライアントに伝えられることが出来ます。
こうすれば新しいクラスを実装することになってもReferee側は変更する必要がないのでプロダクトの拡張もしやすそうですよね。

Discussion