はじめに
こんな記事を読みました。
オブジェクト指向ジャンケン: または石やハサミや紙を手クラスから派生させる是非について
上記の記事を読んで気になった点があったので、本記事を書くことにしました(元記事を書かれた方、勝手にすみません)。
元記事を読んで気になった点
石やハサミや紙クラスを手クラスから派生させることの是非が述べられていない
元記事のタイトルには「石やハサミや紙を手クラスから派生させる是非について」とありますが、元記事ではその是非について述べられていません。
私は継承を 基底クラスの機能を受け継ぐためではなく、概念的に同じものを抽象概念としてまとめるための機能 と捉えています。この考え方に立つと、「グー」「チョキ」「パー」は「手」という抽象概念にまとめることができるため継承を使って良さそうですが、元記事のような継承は すべきではない と思います。元記事の「グー」「チョキ」「パー」はラベル程度の意味しかなく、正しく抽象化できているとは言えないためです。
あくまで一例ですが、じゃんけんプログラムをオブジェクト指向らしく書くとすると、下記のようになるのかなと思います。コードのアイデアは オブジェクトデザイン から得ています。
using System;
namespace JankenTest
{
public interface IHand
{
bool IsStrongerThan (IHand other);
bool IsStrongerThanRock ();
bool IsStrongerThanScissors ();
bool IsStrongerThanPaper ();
}
public class Rock : IHand
{
public bool IsStrongerThan (IHand other)
{
return !other.IsStrongerThanRock ();
}
public bool IsStrongerThanRock ()
{
return true;
}
public bool IsStrongerThanScissors ()
{
return true;
}
public bool IsStrongerThanPaper ()
{
return false;
}
}
public class Scissors : IHand
{
public bool IsStrongerThan (IHand other)
{
return !other.IsStrongerThanScissors ();
}
public bool IsStrongerThanRock ()
{
return false;
}
public bool IsStrongerThanScissors ()
{
return true;
}
public bool IsStrongerThanPaper ()
{
return true;
}
}
public class Paper : IHand
{
public bool IsStrongerThan (IHand other)
{
return !other.IsStrongerThanPaper ();
}
public bool IsStrongerThanRock ()
{
return true;
}
public bool IsStrongerThanScissors ()
{
return false;
}
public bool IsStrongerThanPaper ()
{
return true;
}
}
public class HandFactory
{
public static IHand Create (string handString)
{
if (handString == "R") {
return new Rock ();
}
if (handString == "S") {
return new Scissors ();
}
if (handString == "P") {
return new Paper ();
}
throw new ArgumentException ($"{handString} is invalid hand.");
}
}
public class JankenGame
{
public int Execute (IHand hand1, IHand hand2)
{
bool hand1IsStrongerThanHand2 = hand1.IsStrongerThan (hand2);
bool hand2IsStrongerThanHand1 = hand2.IsStrongerThan (hand1);
return (!hand1IsStrongerThanHand2 && !hand2IsStrongerThanHand1) ? 0 :
(hand1IsStrongerThanHand2) ? 1 : -1;
}
}
}
テストコードも書いています(NUnitを使用)。
using NUnit.Framework;
using System;
namespace JankenTest
{
public class Test
{
[TestCase ("R", "R")]
[TestCase ("S", "S")]
[TestCase ("P", "P")]
public void ExecuteReturnZeroWhenHand1AndHand2AreSame (string hand1, string hand2)
{
var game = new JankenGame ();
var h1 = HandFactory.Create (hand1);
var h2 = HandFactory.Create (hand2);
var actual = game.Execute (h1, h2);
Assert.AreEqual (0, actual);
}
[TestCase ("R", "S")]
[TestCase ("S", "P")]
[TestCase ("P", "R")]
public void ExecuteReturnOneWhenHand1IsStrongerThanHand2 (string hand1, string hand2)
{
var game = new JankenGame ();
var h1 = HandFactory.Create (hand1);
var h2 = HandFactory.Create (hand2);
var actual = game.Execute (h1, h2);
Assert.AreEqual (1, actual);
}
[TestCase ("S", "R")]
[TestCase ("P", "S")]
[TestCase ("R", "P")]
public void ExecuteReturnMinusOneWhenHand2IsStrongerThanHand1 (string hand1, string hand2)
{
var game = new JankenGame ();
var h1 = HandFactory.Create (hand1);
var h2 = HandFactory.Create (hand2);
var actual = game.Execute (h1, h2);
Assert.AreEqual (-1, actual);
}
[TestCase ("r")]
[TestCase ("s")]
[TestCase ("p")]
public void HandFactoryThrowExceptionWhenInvalidHandIsSpecified (string hand)
{
Assert.That (
delegate { return HandFactory.Create (hand); },
Throws.Exception.TypeOf<ArgumentException> ().And.Message.EqualTo ($"{hand} is invalid hand.")
);
}
}
}
「(グー、チョキ、パー以外の)特殊な手を追加したじゃんけん」は「じゃんけん」ではない
例えば誰かに「じゃんけんとは何か?」と質問すると、多くの人は Wikipedia にあるような回答をすると思います。つまり多くの人は「じゃんけん」に「特殊な手」が存在するとは思っておらず、 「特殊な手を追加したじゃんけん」は「じゃんけん」とは別の新しい遊び なのです。
従って、元記事には
このコードにはプレーヤーの増減、ならびに必要に応じて特殊な手の形に対応する余地がある。
とありますが、 個人的には 新しい遊びに対応できるよう設計することはやりすぎで、新しい遊びなので一からプログラムを書けば良いのでは と思っています。
じゃんけんのルールにプレーヤーの手の強弱を判断する以外のロジックを含めるべきではない
元記事のコードを見るとジャンケンのルール( Rule
クラス )には、「手の強弱を判断する」という本来のルールに加え、「手を生成する」や「対戦方式(総当たり戦かトーナメント戦か勝ち抜き戦かなど)を決める」など、「ルール」という単語から想像できない意味も含まれています。
このように単語に独自に意味をもたせてしまうと、メンバー間でのコミュニケーションにも支障が生じ、コードも理解・変更し難いものとなります。
理解しやすいコードであればオブジェクト指向である必要はないのではないか
元記事の最後の主張(下記文章)からは、 コードブロックが短く可能な限り値を不変に保ったコード(つまり、理解しやすいコード) が目指すべきコードであるように読み取れます。
それでも避けがたい仕様変更の惨禍に立ち向かうために私たちにすべきことはオブジェクト指向がしっくりくるとか来ないとかの次元の話ではなく、もっと単純かつ誰もが知っている普遍的なものであるように思う。すなわちコードブロックを短く保つこと、そして可能な限り値を不変に保つことだ。
理解しやすさを求めるのであれば、オブジェクト指向に拘る必要はなく、下記のようなコードで良いのではないでしょうか。
using System;
namespace JankenTest
{
public class Hand
{
public Hand (string hand)
{
if ("RSP".IndexOf (hand, StringComparison.CurrentCulture) == -1) {
throw new ArgumentException ($"{hand} is invalid hand.");
}
Value = hand;
}
public string Value { get; private set; }
}
public class JankenGame
{
public int Execute (Hand hand1, Hand hand2)
{
var h1 = hand1.Value;
var h2 = hand2.Value;
if (h1 == h2) {
return 0;
}
return ("RSPR".IndexOf (h1 + h2, StringComparison.CurrentCulture) == -1) ? -1 : 1;
}
}
}
テストコードも書いています(NUnitを使用)。
using NUnit.Framework;
using System;
namespace JankenTest
{
public class Test
{
[TestCase ("R", "R")]
[TestCase ("S", "S")]
[TestCase ("P", "P")]
public void ExecuteReturnZeroWhenHand1AndHand2AreSame (string hand1, string hand2)
{
var game = new JankenGame ();
var h1 = new Hand (hand1);
var h2 = new Hand (hand2);
var actual = game.Execute (h1, h2);
Assert.AreEqual (0, actual);
}
[TestCase ("R", "S")]
[TestCase ("S", "P")]
[TestCase ("P", "R")]
public void ExecuteReturnOneWhenHand1IsStrongerThanHand2 (string hand1, string hand2)
{
var game = new JankenGame ();
var h1 = new Hand (hand1);
var h2 = new Hand (hand2);
var actual = game.Execute (h1, h2);
Assert.AreEqual (1, actual);
}
[TestCase ("S", "R")]
[TestCase ("P", "S")]
[TestCase ("R", "P")]
public void ExecuteReturnMinusOneWhenHand2IsStrongerThanHand1 (string hand1, string hand2)
{
var game = new JankenGame ();
var h1 = new Hand (hand1);
var h2 = new Hand (hand2);
var actual = game.Execute (h1, h2);
Assert.AreEqual (-1, actual);
}
[TestCase ("r")]
[TestCase ("s")]
[TestCase ("p")]
public void HandThrowExceptionWhenInvalidHandIsSpecified (string hand)
{
Assert.That (
delegate { return new Hand (hand); },
Throws.Exception.TypeOf<ArgumentException> ().And.Message.EqualTo ($"{hand} is invalid hand.")
);
}
}
}
最後に
元記事で述べられている通り、拡張性の高い(変更コストの低い)コードを書くことは重要なことで、そのためのテクニックとしてオブジェクト指向があります。しかしオブジェクト指向は変更コストを下げるためのテクニックの一つにすぎません。他にも const教に入信する や DRYを意識したコードを書く など様々な方法があります。
従ってタイトルにも書いた通り、オブジェクト指向は変更コストを下げるための一つのテクニック程度の認識で、ケースバイケースで適切な手法を採用できれば良いのではないでしょうか。