オブジェクト指向プログラミングで15パズルを作ってみる
オブジェクト指向的でないコードの問題点
前回の記事では、15パズルのプログラムを作りました。しかしこれは「オブジェクト指向でない」あるいは「オブジェクト指向を意識しない」プログラムです。このプログラムには解決したい次のような問題点があります。
(1)コードが長い
すべてのボタンに移動するコードを書かなければならないため、コードが長くなりました。初期化のcase文も分岐が多く長くなっています。長いコードは全体が見通せないため、どこでどんな処理をしているかがわかりにくくなっています。
(2)似たコードの繰り返しになる
似たコードはコピーして使えますが、ボタンの名前を変えるなど単純作業の手間が必要です。単純なコードでも繰り返しの作業には忍耐が必要です。
(3)条件分岐のネストが深い
完成を判定するコードはネストが深く、よく似た条件が重なっており、間違いが起きやすくなります。
これらの問題点を含むため、前回作成したプログラムは間違い、いわゆる「バグ」が生じやすくなります。また「15」パズルは4×4のサイズでタイルの数が16枚ですが、これを5×5の「24」パズルにしようと思えば、コードを全面的に書き換えなければなりません。いわゆる「スケーラブルでない」コードになってしまっています。
オブジェクト指向プログラミングとは
オブジェクト指向を言葉で説明することは難しいのですが、簡潔に言えばプログラムを「オブジェクト」という部品のようなものの集まりとして作り、「オブジェクト」に性質や働きを組み込んでいくプログラミング手法、ということになります。またオブジェクト指向の特徴を示すキーワードに「カプセル化」、「継承」、「多態性」がありますが、必ずしもこれらの特徴すべてを使う必要はありません。必要なものだけ使えばよいのです。
オブジェクト指向によるプログラミング(ボタンクラスの継承)
それではオブジェクト指向を意識しながら15パズルのプログラミングをしましょう。これまでのプログラムは忘れてVisual Studioで新しいプロジェクトを作ってください。
このパズルではタイルを動かすことがプログラムの中心になるので、まずは「タイル」を実現するオブジェクトを作ることを考えます。一から「タイル」オブジェクトを作ることも考えられますが、ここでは既存のオブジェクトを利用することにしましょう。Visual Studioには「ツールボックス」に基本のオブジェクトがたくさん用意されています。その中で、このプログラムで実現したい「タイル」に近いオブジェクトを選びます。やはり「ボタン」がよさそうです。
既存のオブジェクトを使って新しいオブジェクトを作ることを「継承」と言います。「継承」によって定義されたオブジェクトは、元のオブジェクト(ここでは「ボタン」)の機能を持ったものになります。このとき、元の「ボタン」を「基底オブジェクト」、継承して作った新しいオブジェクトを「派生オブジェクト」と言います。「基底オブジェクト」を「親オブジェクト」と、「派生オブジェクト」を「子オブジェクト」と言うこともあります。
オブジェクトの定義はクラスとして記述します。クラスを記述するとき、「基底オブジェクト」を定義するクラスを「基底クラス」と、「派生オブジェクト」を定義するクラスを「派生クラス」と言います。基底オブジェクトを継承して新しいオブジェクトを定義するコードは次のようになります。このコードではButtonクラスを基底オブジェクトとしてCustomButtonクラスを作ります。CustomButtonクラスはButtonクラスのすべての機能を持ったオブジェクトになります。
class CustomButton : Button { }
では、デザイナでマウスを右クリックして「コードの表示」をしてコードを書きましょう。コードは次のようになります。プログラムのまとまりは、初めと終わりを波カッコ「{}」で囲みますので、クラスを書く場所を間違えないようにしてください。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace SampleProject2 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } } class CustomButton : Button { } }
このままではカスタムボタンは基底クラスのButtonクラスとまったく同じ機能でしかありません。では次に、このカスタムボタンクラスに独自の機能を加えましょう。
カスタムボタンにフィールドを定義する
カスタムボタンに、継承元の基底クラスであるButtonクラスにはない独自の機能を加えましょう。まずカスタムボタンに、自分の状態を示す次の2つの値を持たせることにします。
ひとつはボタンの位置を示す値で、パズルの左上から右下に向けて1から16の値をとることに決めます。この値は、後でゲームの完了を判定するために使うので、型を文字列型で定義し、名前をPositionとします、
もうひとつは、ボタンが空白のタイルにあたる16番目のボタンと交換可能な位置にあるか否かを示す値で、trueかfalseかの2つの状態を区別すればよいのでbool型で定義し、名前をisMovableとします。このようにクラスに持たせるデータのことを「フィールド」または「メンバ変数」などと呼びます。オブジェクト指向で「クラスはデータとメソッドをひとまとめにしたものである」と言うときの「データ」にあたるものです。クラスにフィールドを持たせるカスタムボタンのコードは次のようになります。
class CustomButton : Button { public string Position; public bool isMovable; }
カスタムボタンにコンストラクタを定義する
クラスとして定義されたオブジェクトは、プログラムを実行するときに生成されます。この生成されるオブジェクトの実体を「インスタンス」と言います。オブジェクトの生成の際に、初期化するコードを「コンストラクタ」と言います。コンストラクタはクラス内にクラスと同じ名前で定義します。
次のコードでは、カスタムボタンが実体化してインスタンスを生成するときに、大きさを縦横100ピクセルに、表示する文字のフォントをArialで20ポイントに、カスタムボタンが移動可能かを判定するisMovableフィールドの値をfalseにします。
public CustomButton() { this.Width = 100; this.Height = 100; this.Font = new Font("Arial", 20); this.isMovable = false; }
ここまでのコード全体は次のようになります。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace SampleProject2 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } } class CustomButton : Button { public string Position; public bool isMovable; public CustomButton() { this.Width = 100; this.Height = 100; this.Font = new Font("Arial", 20); this.isMovable = false; } } }
カスタムボタンにメソッドを定義する
カスタムボタンに基底クラスのボタンにない振る舞いを追加しましょう。振る舞いの定義は「メソッド」と言います。メソッドは引数を持たせることができ、プログラムから引数を指定して呼び出すと、メソッド内のコードが実行されます。
カスタムボタンをインスタンスに実体化したときに、共通の要素であるボタンの大きさやフォントなどについてはコンストラクタで定義しました。カスタムボタンは15パズルのタイルになるので、空白の16番目のタイルを含めて16個のインスタンスを実体化することになります。このとき、個々のインスタンスによって異なる要素であるNameとPositionのフィールドの値、表示する文字、位置を決めるTopとLeftの値を決める処理を定義しましょう。このメソッドの名前はInitialize()とします。
次のコードでは、メソッドにint型の整数値iの引数を持たせ、iの値で要素が決まるようにしています。最後の行では表示する文字を上書きしています。これはテスト用のコードで、ゲームの処理において重要な各カスタムボタンの要素を表示して、正しい処理が行われていることを確認するためのものです。ゲームが完成すれば削除することになります。この行には、行の終わりに//で始まるコメント文をつけています。
public void Initialize(int i) { this.Name = (i + 1).ToString(); this.Position = (i + 1).ToString(); this.Text = (i + 1).ToString(); this.Top = (i / 4) * 100; this.Left = (i % 4) * 100; this.Text = this.Name + this.isMovable + this.Position;//テスト用のコード }
次にボタンをクリックしたときに、空白タイルである16番目のボタンと位置を交換するメソッドを作ります。このメソッドの名前はMove()とします。ここで引数に、カスタムボタンクラスの型を持たせることにします。ここがオブジェクト指向的なプログラムの勘所と言えます。メソッドの概念は、一般的な数学の知識でいう関数の概念に近いものです。関数は学校で習いますし、エクセルなどのアプリケーションで関数を使うことがありますが、このときに引数になるのは数字か文字列です。しかしオブジェクト指向プログラミングでは、メソッドの引数に他の型を使うことができるのです。もう少し詳しく言えば、整数型のintや文字列型のstringなどC#言語にあらかじめ用意されている型は「組み込み型」と言い、これらはクラスの一種なのです。
次のコードでは、メソッドの引数にCustomButtonクラスのオブジェクトを持たせ、自分の位置を引数のオブジェクトの位置と交換する処理をします。実世界の例えで説明すると、いくつかの座席があって人が座っている状態をイメージしてください。このときに、座席を交代してほしい人が「〇〇さん、座席を変わってください」と言うようなものです。このときに名前を呼ぶ「〇〇さん」のところがメソッドの引数にあたります。
メソッドの中では渡された引数、位置を交換する相手のオブジェクトは、cbという名前で処理されます。メソッドの始めに自分自身が位置交換可能かどうかをisMovableフィールドの値で確かめ、交換可能な状態であれば交換処理を実行します。交換処理のために一時的な変数tempTop、tempLeft、tempPositionを使っています。
public void Move(CustomButton cb) { if (this.isMovable == true) { int tempTop; int tempLeft; string tempPosition; tempTop = this.Top; tempLeft = this.Left; tempPosition = this.Position; this.Top = cb.Top; this.Left = cb.Left; this.Position = cb.Position; cb.Top = tempTop; cb.Left = tempLeft; cb.Position = tempPosition; } }
これでカスタムボタンのクラスを定義することができ、カスタムボタンオブジェクトを生成する用意ができました。ここでコードは次のようになっています。ここまでできたら、一度Visual Studioで緑の三角「開始」ボタンをクリックするか、「デバッグ」メニューの「デバッグの開始」でビルドしてプログラムを実行しましょう。
ここまでのプログラムでは、カスタムボタンクラスを作りましたが、まだオブジェクトはインスタンスとして実体化していません。ですので、実行してもメニューバーだけがあるウィンドウしか表示され、そこには何もありません。ここではこれでよいので、エラーが出ないことだけを確かめてください。
ここまでのコード全体は次のようになります。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace SampleProject2 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } } class CustomButton : Button { public string Position; public bool isMovable; public CustomButton() { this.Width = 100; this.Height = 100; this.Font = new Font("Arial", 20); this.isMovable = false; } public void Initialize(int i) { this.Name = (i + 1).ToString(); this.Position = (i + 1).ToString(); this.Text = (i + 1).ToString(); this.Top = (i / 4) * 100; this.Left = (i % 4) * 100; this.Text = this.Name + this.isMovable + this.Position;//テスト用のコード } public void Move(CustomButton cb) { if (this.isMovable == true) { int tempTop; int tempLeft; string tempPosition; tempTop = this.Top; tempLeft = this.Left; tempPosition = this.Position; this.Top = cb.Top; this.Left = cb.Left; this.Position = cb.Position; cb.Top = tempTop; cb.Left = tempLeft; cb.Position = tempPosition; } } } }