はじめに
オブジェクト指向が5000%理解できる記事(実践編) という記事がとても興味深かったので、僕も実践してみたいと思い、初めて投稿してみました。
僕自身のオブジェクト指向の捉え方
僕は、オブジェクト指向(object-oriented)の object という単語は、subject の対義語の意味での object、すなわち客体・目標物だと捉えています。
※ 記事を書いている途中で調べてみたのですが、Wikipedia の オブジェクト指向 もそのような流れで説明されていますね。
とはいえ、オブジェクト指向に明るくない方には「オブジェクトって『モノ』なんですよ」と説明することが多いです。ただし、『物』ではなく『モノ』と言ってます。物理的な存在に限らず、抽象的な概念だったりすることもあるので、なかなか難しいものだと、説明することが多いです。
予めお詫び
実装例は、僕の都合で申し訳ないのですが、割と書き慣れている Java で書いています。今、手元にコンパイラを通せる環境がないので、エラーあれば後ほど修正します。Javadoc も書いていません。予めお詫び申し上げます。
実践してみる
上記を踏まえて、問題に取り組んでみます。
問題の全文は申し訳ないですが引用元記事を参照願います。
A~C の 3 人のプレイヤーが、1~5 までのカードを使ってインディアンポーカーのようなゲームをするという内容です。
クラスを洗い出す前に
現実世界でこのゲームをプレーするシーンをイメージして、そこからクラスを洗い出すというのは素直なアプローチだと思います。しかし、僕の感覚では、現実世界は少々世知辛いんじゃないかなと思っています。
何が言いたいのかというと、この問題を見たときに、登場する人物は「3 人」と考えるのがおそらくは自然だと思います。しかし、厳密にいえば、プレイヤーの他に「ゲームの進行を管理する人」や「正しく回答しているかを判断する人」も必要なはずです。(そもそも、それは「人」でなくてもいいのかもしれませんが。)
残念ながら、現実世界ではそのような人は用意されないのが普通だと思います。プレイヤーが各々で進行を管理したり回答の正しさを判断するでしょう。
プログラミングの世界では現実現実の制約に縛られずに物事を考えることができるので、せっかくなので、現実世界よりももう少し理想的な世界を考えた方が幸せになれるんじゃないかなと思っています。
クラスを洗い出しつつ、状態や振る舞いもちょこちょこと足していく
まず、プレイヤーとカードは必要ですね。
プレイヤーは名前を持たせるようにしておきます。さらに、各プレイヤーは 1 枚のカードを持つので、フィールドとゲッター/セッターを用意しておきます。
public class Player {
private String name;
private Card card;
public Player(String name) {
this.name = name;
}
public void getName() {
return name;
}
public void getCard() {
return card;
}
public void setCard(Card card) {
this.card = card;
}
}
カードの数字は、最初に決めたら後から変わらないので、コンストラクタで設定するようにして、ゲッターだけ用意しておきます。このようにすることで、いわゆる不変な(immutable)オブジェクトになります。
※ 説明を飛ばしていましたが、Player の name も immutable です。
public class Card {
private int number;
public Card(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
}
次に、カードの集合を「山札」という別のクラスで表現します。もちろん単なる Card のリストでも表現できるのですが、「シャッフルして上から 1 枚ずつ引く」という行為がイメージされることから、「山札という概念」をクラスとして抽出しておくと何かと便利なはずです。
public class Deck {
private List<Card> cards;
public Deck() {
// 簡単のため、コンストラクタで 1~5 のカードを一式として作っておく
cards = new ArrayList<>();
for(int i = 0; i < 5; i++) {
cards.add(new Card(i + 1));
}
}
public void shuffle() {
// 簡単のため、Collection API でシャッフルしている
Collections.shuffle(cards);
// お好みで、ショットガンシャッフルなど実装してもよい
}
// 俺のターン!にこのメソッドを呼べば、上から 1 枚カードを引ける
public Card draw() {
return cards.remove(0);
}
}
次に、「ゲームの進行を管理する人」を登場させます。この人は、全プレイヤーにカードを配って、A さんから順番に答えさせていきます。
クラスの名前は、本当はもっといい名前がありそうですが、カジノのテーブルのようなものをイメージして、ディーラーにしておきます。ディーラーがプレイヤーを 3 人囲っているイメージで進めます。
public class Dealer {
private List<Player> players;
public Dealer() {
// 簡単のため、コンストラクタで 3 人を囲っておく
players = new ArrayList<>();
players.add(new Player("A"));
players.add(new Player("B"));
players.add(new Player("C"));
}
// カードをプレイヤーに配る(メソッド名は deal でいいのか怪しいですが)
public void deal() {
// 新しい山札をシャッフルして
Deck deck = new Deck();
deck.shuffle();
// 各プレイヤーに 1 枚ずつ配る
for(Player player: players) {
Card card = deck.draw();
player.setCard(card);
}
}
ここまで来たら、あとはゲームの結果を判定するだけになります。そして、よくよく考えると、結果の判断も各プレイヤーが他の全プレイヤーのカードの数値を見ながらいちいち判断しなくても、ディーラーが A さんから順番にカードの内容を見て判断しても結果が変わらないことに気づいてしまいます。(ゲームとしては全く味気ないですが…)
public class Dealer {
// (前述の内容は省略)
// 厳密にいえば、このメソッドは deal メソッドが呼ばれた後でないと呼び出せないように工夫しておいた方がよい
public void judge() {
// この中で、A さんのカードから順番に判定する処理を作る
int currentPlayerIndex = 0;
while(true) {
Player currentPlayer = players.get(currentPlayerIndex);
int currentPlayerNumber = player.getCard().getNumber();
// 具体的な判定処理は省略。
// (真面目に考えると、この作りだと少々変な気もしますが、
// 雰囲気だけ伝わればいいかなという感覚です。)
// 結果がわかれば繰り返し処理を抜ける(条件判定の変数は適当です)
if(result != UNKNOWN) {
break;
}
// 結果がわからなければ、次のプレイヤーへ
currentPlayerIndex++;
currentPlayerIndex %= players.size();
}
}
}
というのが、いわゆる「ぼくのかんがえたさいきょうの」実装例となります。
さいきょうの、はさすがに嘘です。ごめんなさい。
プログラミング対象の規模や関心事にあわせて、クラス分割の粒度や、どのメソッドをどのクラスに実装するかなどを都度考えるのですが、与えられた題材くらいの規模感であれば上述のように考えます。
おわりに
頭の中で色々と考えてみて、記事という形でアウトプットすることによって、自分の中でも気づきが得られたので、少しはオブジェクト指向の理解が深まってきたかなと思います。
「これが正解」というものではないとは思いますが、考え方のひとつとして皆さんの参考になれば幸いです。