【C#】カードをシャッフルするメソッドはどこに置くべき?CardとDeckの責務を考える

広告

トランプのカードを表すCard構造体を作ったとき、「カードをシャッフルするメソッドはどこに置けばよいのだろう?」と迷うことがあります。

一見するとCard.Shuffle(cards)でもよさそうですが、メソッドの置き場所を考えるときに大切なのは、その処理が何に対する操作なのかです。

この記事では、1枚のカードを表すCardと、カードの集合を表すDeckの役割を分けながら、読みやすく拡張しやすい設計を考えます。

1枚のCardと、Shuffleメソッドを持つDeckの責務の違いを表したイラスト
1枚のカードはCard、カードの集合をシャッフルする責務はDeckに持たせます。

出発点となるCard構造体

まずは、1枚のトランプを表すCard構造体を見てみましょう。

using System;

namespace List11_1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var card1 = new Card('H', 8);
            var card2 = new Card('S', 8);

            Card[] cards = new Card[52];
            cards[0] = card1;
            cards[1] = card2;

            cards[1].Print();
        }
    }

    struct Card
    {
        public char Suit { get; private set; }
        public int Number { get; private set; }

        public Card(char suit, int number)
        {
            Suit = suit;
            Number = number;
        }

        public void Print()
        {
            var s = "";

            switch (Suit)
            {
                case 'H': s = "ハート"; break;
                case 'D': s = "ダイヤ"; break;
                case 'S': s = "スペード"; break;
                case 'C': s = "クラブ"; break;
            }

            Console.WriteLine($"{s} {Number}");
        }
    }
}

Cardは「スートと数字を持つ1枚のカード」です。そのカード自身を表示するPrint()Cardにあるのは自然です。

cards[1].Print();

コードを読んだときにも、「2枚目のカードを表示する」と素直に理解できます。

ShuffleをCardに置くと何が気になるのか

では、52枚のカードをシャッフルする処理はどこに置くべきでしょうか。

Card.Shuffle(cards);

このような静的メソッドをCardに追加することは技術的には可能です。しかし、Cardは1枚のカードを表す型なのに、Shuffleは複数枚のカードを並べ替える処理です。

つまり、操作の対象が一致していません。

  • Print():1枚のカードに対する操作
  • Shuffle():カードの集合に対する操作

CardShuffle()を持たせると、1枚のカードを表す責務と、複数枚を管理する責務が同じ型に混ざります。コードが小さいうちは困らなくても、機能が増えるにつれて「どこに何がある型なのか」が分かりにくくなります。

山札を表すDeckクラスを作る

カードの集合に対する操作なら、集合そのものを表す型を用意すると自然です。トランプの場合、その型はDeck(山札)と名付けられます。

class Deck
{
    private readonly Card[] cards = new Card[52];

    public Deck()
    {
        char[] suits = { 'H', 'D', 'S', 'C' };
        int index = 0;

        foreach (char suit in suits)
        {
            for (int number = 1; number <= 13; number++)
            {
                cards[index] = new Card(suit, number);
                index++;
            }
        }
    }

    public void Shuffle()
    {
        for (int i = cards.Length - 1; i > 0; i--)
        {
            int j = Random.Shared.Next(i + 1);
            (cards[i], cards[j]) = (cards[j], cards[i]);
        }
    }

    public Card this[int index] => cards[index];

    public int Count => cards.Length;
}

Deckのコンストラクターで52枚のカードを生成し、Shuffle()で山札全体を並べ替えています。インデクサーを用意しているため、deck[0]のように1枚ずつ取り出すこともできます。

呼び出し側は次のようになります。

var deck = new Deck();

deck.Shuffle();
deck[0].Print();

deck.Shuffle()は、ほぼ日本語の「山札をシャッフルする」そのままです。処理の持ち主と操作の対象が一致しているため、コードの意図をすぐに読み取れます。

シャッフルにはFisher–Yates法を使う

上のShuffle()では、末尾から順番にランダムな位置のカードと交換しています。これはFisher–Yates(フィッシャー–イェーツ)シャッフルと呼ばれる方法です。

for (int i = cards.Length - 1; i > 0; i--)
{
    int j = Random.Shared.Next(i + 1);
    (cards[i], cards[j]) = (cards[j], cards[i]);
}

各位置について交換相手を選ぶため、すべての並び順を偏りなく生成できます。OrderBy(x => Random.Shared.Next())のような書き方よりも、シャッフルの意図が明確で効率的です。

Randomを毎回newしない

Shuffle()を呼ぶたびにnew Random()を作る方法もありますが、短い間隔で繰り返し生成すると、古い.NET環境では同じ乱数列になる可能性があります。

.NET 6以降なら、共有インスタンスであるRandom.Sharedを使うのが簡単です。古い環境では、RandomDeckのフィールドとして1つだけ保持する方法がよいでしょう。

private static readonly Random random = new Random();

Deckに置くことで機能を自然に増やせる

Deckを作る利点は、Shuffle()の置き場所が分かりやすくなることだけではありません。山札に関する機能を、同じ場所へ自然に追加できます。

  • 山札から1枚引く
  • 残り枚数を調べる
  • 山札を最初の状態に戻す
  • 捨て札を山札へ戻す
  • カードが残っているか確認する

たとえば「1枚引く」処理を追加するなら、利用側は次のように書けます。

Card card = deck.Draw();

「誰がその処理を担当するのか」が型名とメソッド名から伝わるので、プログラム全体が読みやすくなります。

配列を汎用的に混ぜたい場合はどうする?

ゲーム全体でさまざまな配列やリストをシャッフルしたい場合は、Deckではなく、汎用的なヘルパーメソッドや拡張メソッドとして用意する選択肢もあります。

public static class ListExtensions
{
    public static void Shuffle<T>(this IList<T> items)
    {
        for (int i = items.Count - 1; i > 0; i--)
        {
            int j = Random.Shared.Next(i + 1);
            (items[i], items[j]) = (items[j], items[i]);
        }
    }
}

これは「山札という概念の操作」ではなく、「コレクションを並べ替える汎用処理」として設計した場合です。

判断のポイントは、次のように整理できます。

  • 山札のルールとしてシャッフルする:Deck.Shuffle()
  • あらゆるコレクションを汎用的に混ぜる:拡張メソッド

同じアルゴリズムでも、何を表現したいかによって適切な置き場所は変わります。

まとめ

  • Cardは「1枚のカード」の責務だけを持たせる
  • 複数のカードをまとめて管理する責務はDeckへ切り出す
  • シャッフルはカード1枚ではなく山札に対する操作なので、Deck.Shuffle()が自然
  • シャッフルの実装にはFisher–Yates法を使える
  • 汎用的なコレクション操作にしたい場合は拡張メソッドも選択肢になる

メソッドの置き場所に迷ったら、「この処理は誰の仕事なのか」「操作の対象は何か」を考えてみてください。

型の責務を意識するだけで、コードの意味が伝わりやすくなり、あとから機能を追加するときにも迷いにくくなります。

訪問数 2 回, 今日の訪問数 2回

広告