オブジェクト指向ジャンケン: または石やハサミや紙を手クラスから派生させる是非について

何かを見てしまった結果、以下のようなコードを書いてしまった。このプログラムは任意の人数でジャンケンをした結果をシミュレーションできる。

```csharp:Program.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace OOP.HasNotComeSikkuri {

	public abstract class Hand {
	}

	public class Gu : Hand {
		public override string ToString()
			=> "グー";
	}

	public class Tyoki : Hand {
		public override string ToString()
			=> "チョキ";
	}

	public class Pa : Hand {
		public override string ToString()
			=> "パー";
	}

	public interface IComparer {
		Type From { get; }
		Type To { get; }
		bool Result { get; }
	}

	public sealed class Wins<T1, T2> : IComparer
		where T1 : Hand
		where T2 : Hand
	{
		public Type From { get; } = typeof(T1);
		public Type To { get; } = typeof(T2);
		public bool Result { get; } = true;
	}

	public sealed class Reverse : IComparer {
		public Type From { get { return _Source.To; } }
		public Type To { get { return _Source.From; } }
		public bool Result { get { return !_Source.Result; } }
		readonly IComparer _Source;
		public Reverse(IComparer source)
		{
			_Source = source;
		}
	}

	public sealed class Rule {
		readonly ReadOnlyDictionary<string, IComparer> _Matches;
		readonly Type[] _AvailableHands;

		public Rule(params IComparer[] rules)
		{
			_Matches = new ReadOnlyDictionary<string, IComparer>(
				rules
					.SelectMany(rule => new[] { rule, new Reverse(rule) })
					.GroupBy(rule => GenerateMatchKey(rule.From, rule.To))
					.ToDictionary(gr => gr.Key, gr => gr.Last())
				);
			_AvailableHands = rules.SelectMany(rule => new[] { rule.From, rule.To }).Distinct().ToArray();
		}

		private string GenerateMatchKey(Type t1, Type t2)
			=> $"{t1}=>{t2}";

		public Hand CreateHand(int randomNumber)
			=> Activator.CreateInstance(_AvailableHands[(randomNumber % _AvailableHands.Length)]) as Hand;

		public Hand Play(params Hand[] players)
			=> players.SingleOrDefault(player => players.All(target => target == player || Win(player, target)));

		private bool Win(Hand player1, Hand player2)
			=> (Find(player1, player2, true) ?? Find(player2, player1, false)) != null;

		private IComparer Find(Hand player1, Hand player2, bool expectedResult)
		{
			IComparer rule;
			return _Matches.TryGetValue(GenerateMatchKey(player1.GetType(), player2.GetType()), out rule) && (rule.Result == expectedResult) ? rule : null;
		}
	}

	public class Program {
		public static void Main()
		{
			int numOfPlayers;
			while (true) {
				Console.Write("プレーヤー人数を入力してください:");
				if (int.TryParse(Console.ReadLine(), out numOfPlayers) && 0 <= numOfPlayers) {
					break;
				}
			}

			var rule = new Rule(new Wins<Gu, Tyoki>(), new Wins<Tyoki, Pa>(), new Wins<Pa, Gu>());
			var rand = new Random();
			while (true) {
				var players = new Hand[numOfPlayers];
				for (int i = 0; i < numOfPlayers; i++) {
					var player = rule.CreateHand(rand.Next());
					Console.WriteLine($"プレーヤー{i + 1}:{player}");
					players[i] = player;
				}

				var winner = rule.Play(players);
				if (winner != null) {
					var index = Array.IndexOf(players, winner);
					Console.WriteLine($"プレーヤー{index + 1}の勝ちです");
				}
				else {
					Console.WriteLine("引き分けです");
				}
				Console.WriteLine();
				Console.WriteLine("Enterキーで終了します");
				if (Console.ReadKey().Key == ConsoleKey.Enter) {
					break;
				}
				Console.WriteLine("");
			}
		}
	}
}
```

このコードにはプレーヤーの増減、ならびに必要に応じて特殊な手の形に対応する余地がある。オブジェクト指向プログラミングの大きなメリットは再利用性だとされた時代もあったが、個人的にはどちらかというとプログラムの拡張性を保ちやすいのが有り難く思える。この点については、それこそC言語でもポインタだらけの怪しげな構造体を準備して再現を試みようとするくらいには有益だ。

問題は顕在化しない仕様変更への耐性にお金を出してくれるお客様はまずいないこと。そして仕様変更はしばしば想定の斜め上を行くため、これだけのコードを書いても対応できない特殊ルールもありえるという点にある。

例えばジャンケン十三奥義のひとつである"科学力"は**「高温のはんだごてを相手の手に押し付ける」**という技だ。これが発動した場合、参加プレーヤーが一人以上減ることが予期されるが、上記の設計でそれを完全に再現することはできない。

それでも避けがたい仕様変更の惨禍に立ち向かうために私たちにすべきことはオブジェクト指向がしっくりくるとか来ないとかの次元の話ではなく、もっと単純かつ誰もが知っている普遍的なものであるように思う。すなわちコードブロックを短く保つこと、そして可能な限り値を不変に保つことだ。手が石にされたりハサミと交換されてしまうことなど、プログラムの値がいつの間にか変わっている恐怖に比べれば些細なものではないか。

さあ、君もconst教に入りたまえ。