6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ジャンケンゲームで学ぶ、C#オブジェクト指向設計例

Last updated at Posted at 2019-07-07

この記事の目的について

本記事の目的は、皆さんがオブジェクト指向でプログラムを作っていく上で発生する、
「設計はどのようにすれば良いのだろう?」となった時の指標となるような、
極限まで分解された理想的な設計の1例を示すことです。

概要

C#でジャンケンを作っていきたいと思います。
具体的な仕様をまずは考えていきます。

  • 環境はコンソールアプリケーション。
  • ジャンケンの手を選んでください(1=グー,2=チョキ,3=パー)」と表示させる。
  • ユーザーは1,2,3のいずれかを入力する。(それ以外を入力されたら、再度入力するよう要求)。
  • AIがランダムで手を出す。
  • 勝敗を出力する。

まあ、だいたいこんな感じでしょうか。

アーキテクチャとしては、レイヤードアーキテクチャを意識しつつやってみます。

完成図

image4.png

ロジック分析

早速開発!っと行きたい所ですが、まずはジャンケンについて深く分析しておきます。

  • ジャンケンは複数の人間で遊ぶことができる。
  • ジャンケンの勝敗の種類は勝ち、負け、引き分けがある。
  • ジャンケンのルール人間全員が共通理解した上でジャンケンは行われる。
  • 人間はグー、チョキ、パーいずれかのを出し、他の人間達の出したと見比べ、自身の勝敗の種類ジャンケンのルールに基づいて知ることができる。

ここで、ジャンケンに存在するオブジェクトを抽出していきましょう。

  • 勝敗の種類
  • ジャンケンのルール
  • 人間

ですね。
この三つがあればジャンケンの純粋なロジック部分を実装できそうです。

早速これらをコードに落とし込んでいきましょう。

ロジックの実装

まずは勝敗の種類です。
勝敗の種類は勝ち、負け、引き分けでしたのでそれを表現していきましょう。

ResultKind.cs
namespace Domain
{
    // 勝敗の種類
    enum ResultKind
    {
        Win,
        Lose,
        Draw
    }
}

次は手です。
手はグー、チョキ、パーの種類を持つオブジェクトですのでそれを表現していきましょう。

HandKind.cs
namespace Domain
{
    // 手の種類
    enum HandKind
    {
        Guu,
        Tyoki,
        Paa
    }
}

次にジャンケンのルールです。
ジャンケンのルールとは、手の組み合わせから勝敗の判定をするものです。
ジャンケンのルールはローカルルールなどで、複数ある可能性があります。
そこも考慮しつつ、コードに落とし込んでみましょう。

IRule.cs
using System.Collections.Generic;

namespace Domain
{
    // ジャンケンのルールを表すインターフェイス
    interface IRule
    {
        // 勝敗を判定する
        ResultKind Judge(HandKind myHand, IEnumerable<HandKind> otherHands);
    }
}

次に実際のジャンケンのルールを1つ定義しておきましょうか。
もっとも一般的なジャンケンのルールを定義したいと思います。

StandardRule.cs
using System.Collections.Generic;

// Enum名の省略
using static Domain.HandKind; 
using static Domain.ResultKind;

namespace Domain
{
    // もっとも一般的なジャンケンのルール
    class StandardRule : IRule
    {
        // 勝敗を判定する
        public ResultKind Judge(HandKind myHand, IEnumerable<HandKind> otherHands)
        {
            uint winCount = 0;
            uint loseCount = 0;
            foreach (var otherHand in otherHands)
            {
                var resultKind = Judge(myHand, otherHand);
                if (resultKind == Win) winCount++;
                else if (resultKind == Lose) loseCount++;
            }
            if (winCount * loseCount != 0 || winCount + loseCount == 0) return Draw;
            if (winCount != 0) return Win;
            return Lose;
        }

        // 1対1の時の勝敗判定
        private ResultKind Judge(HandKind myHand, HandKind otherHand)
        {
            if (myHand == otherHand) return Draw;
            if (
                (myHand == Guu && otherHand == Paa) ||
                (myHand == Tyoki && otherHand == Guu) ||
                (myHand == Paa && otherHand == Tyoki)
                )
                return Lose;
            return Win;
        }
    }
}

最後に人間を表現していきましょう。

人間は、

  • ジャンケンのルール人間全員が共通理解した上でジャンケンは行われる。

  • 人間はグー、チョキ、パーいずれかのを出し、他の人間達の出したと見比べ、自身の勝敗の種類ジャンケンのルールに基づいて知ることができる。

でした。

ポイントは

  • 同じジャンケンのルールを共有した人間どうしでのみジャンケンができる。
  • 手を出すことができる。
  • 自分の出した手と他の人間の出した手から勝敗を知ることができる。

です。

Human.cs
using System.Linq;

namespace Domain
{
    // ジャンケンをする人間を表す
    class Human<Rule> where Rule : IRule
    {
        private readonly Rule rule;
        public HandKind Hand { get; }

        // ルールと出す手をセット
        public Human(Rule rule, HandKind hand)
        {
            this.rule = rule;
            this.Hand = hand;
        }

        // 勝敗を知る
        public ResultKind KnowResult(Human<Rule>[] otherHumen)
            => rule.Judge(Hand, otherHumen.Select(x => x.Hand));
    }
}

コツは、Human<Rule>という風にジェネリクスを使い、同じルールの人間としかジャンケンができなくした点です。

入出力の実装

ひと通り、重要なロジックの実装は完成したので、次は入出力の処理を実装していきましょう。

・「ジャンケンの手を選んでください(1=グー,2=チョキ,3=パー)」と表示させる。
・ユーザーは1,2,3のいずれかを入力する。(それ以外を入力されたら、再度入力するよう要求)。
です。

依存を分離しておきたいので、先に入出力のインターフェイスを定義します。

IView.cs
using System;
using System.Collections.Generic;

namespace Application
{
    // ユーザーとのやりとりをするインターフェイス
    interface IView
    {
        event Action<string> OnInput;

        // 入力を促す
        void ShowRequest();
        
        // 再入力を促す
        void ShowTryRequest();

        // 勝敗を出力する
        void ShowResult(string result, string myHand, IEnumerable<string> aiHands);
    }
}

これを元にコンソールの入出力を実装したクラスを作成します。

ConsoleView.cs
using System;
using System.Collections.Generic;

namespace UserInterface
{
    // コンソールの入出力をするクラス
    class ConsoleView : Application.IView
    {
        public event Action<string> OnInput;

        // 入力を促す
        public void ShowRequest()
        {
            Console.WriteLine("ジャンケンの手を選んでください(1=グー,2=チョキ,3=パー)");
            var input = Console.ReadLine();
            OnInput(input);
        }
        
        // 再入力を促す
        public void ShowTryRequest()
        {
            Console.WriteLine("もう一度入力してください");
            ShowRequest();
        }

        // 勝敗を出力する
        public void ShowResult(string result, string myHand, IEnumerable<string> aiHands)
        {
            Console.WriteLine("あなた:" + myHand);
            foreach (var aiHand in aiHands)
                Console.WriteLine("AI:" + aiHand);
            Console.WriteLine("あなたは" + result + "です");
        }
    }
}

これで、入出力とジャンケンゲームのロジックは完成しました。
でもこれだけでは、まだ足りないところがあります。
最後に、入出力とロジックを上手くつなぎ合わせるコードを書いていきましょう。

つなぎ合わせる

入出力では文字でやりとりしていますが、
ジャンケンロジックの方では、Enumを使って値をやり取りしています。
そのため、両者には値の変換作業が必要です。
まずはそれをするクラスを作ります。

StringConverter.cs
using Domain;

namespace Application
{
    // 文字列と相互変換する
    static class StringConverter
    {
        // 文字列をHandKindへ変換("1"=グー,"2"=チョキ,"3"=パー)
        static public HandKind? StringToHandKind(string consoleString)
        {
            int result;
            if (int.TryParse(consoleString, out result) == false) return null;
            if (result < 1 || result > 3) return null;
            var handKinds = new HandKind[] { HandKind.Guu, HandKind.Tyoki, HandKind.Paa };
            return handKinds[result - 1];
        }

        // ResultKindを文字列へ変換
        static public string ResultKindToString(ResultKind resultKind)
        {
            switch (resultKind)
            {
                case ResultKind.Draw:
                    return "引き分け";
                case ResultKind.Win:
                    return "勝利";
                default:
                    return "敗北";
            }
        }

        // ResultKindを文字列へ変換
        static public string HandKindToString(HandKind handKind)
        {
            switch (handKind)
            {
                case HandKind.Guu:
                    return "グー";
                case HandKind.Tyoki:
                    return "チョキ";
                default:
                    return "パー";
            }
        }
    }
}

次にユーザーと対戦する、ランダムな手を出す人間を生成するクラスを作っておきます。

AiHumanCreator.cs
using System;
using Domain;

namespace Application
{
    // Aiの人間を生成するクラス
    class AiHumanCreator
    {
        private readonly Random rand;

        public AiHumanCreator(Random rand) => this.rand = rand;

        // Aiの人間を複数生成する
        public Human<Rule>[] CreateAiHumen<Rule>(Rule rule, uint size) where Rule : IRule
        {
            var handKinds = new HandKind[] { HandKind.Guu, HandKind.Tyoki, HandKind.Paa };
            var aiHumen = new Human<Rule>[size];
            for (int i = 0; i < aiHumen.Length; i++)
            {
                var aiHandKind = handKinds[rand.Next(3)];
                aiHumen[i] = new Human<Rule>(rule, aiHandKind);

            }
            return aiHumen;
        }
    }
}

これで、最後です。
今までの全てをつなぎ合わせ、ジャンケンゲームをするクラスを作ります。

Game.cs
using System.Linq;
using Domain;

namespace Application
{
    // ゲームを表す
    class Game
    {
        // ジャンケンゲームを開始する
        public Game(IView view, AiHumanCreator aiHumanCreator)
        {
            view.OnInput += input =>
            {
                var handKind = StringConverter.StringToHandKind(input);
                if (handKind == null)
                {
                    view.ShowTryRequest();
                    return;
                }
                var player = new Human<StandardRule>(new StandardRule(), (HandKind)handKind);
                var aiHumen = aiHumanCreator.CreateAiHumen(new StandardRule(), 2);
                view.ShowResult(
                    StringConverter.ResultKindToString(
                        player.KnowResult(aiHumen)
                    ),
                    StringConverter.HandKindToString(player.Hand),
                    aiHumen.Select(x => StringConverter.HandKindToString(x.Hand))
                );
            };
            view.ShowRequest();
        }
    }
}

後は、Main関数から呼び出します。

Program.cs
using System;
class Program
{
    static void Main(string[] args)
    {
        new Application.Game(
            new UserInterface.ConsoleView(),
            new Application.AiHumanCreator(new Random())
        );
    }
}

アーキテクチャ

今回のプログラムの全体図を載せておきます。

ジャンケンゲームアーキテクチャ (1).png

終わりに

これは、私が設計の練習として思考したものを記事にしたものです。
未熟ですので、「もっとこうした方が良いよ」などのマサカリは歓迎します。
少しでも設計の参考になることを願います。

ソースコード

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?