#はじめに
C#を使い始めて5か月くらいになり、業務でのコードのボリュームが増えてきたため、スパゲッティを卒業してきれいなコードを書いてみたいと思い始めました。
今まで業務とは関係のないコーディングをしていた頃は、自分一人のためのコーディングであったため、誰かに見せるという機会がなくそれでいて保守性や拡張性等を考えたこともありませんでした。
機能を追加したいと考えたときに過去に書いたコードを編集しないといけないようなコードを書き続けていると、時間を食うようになりこれではいけないと思いコーディングのいろは的なものを調べてみました。
調べていたところオブジェクト指向やデザインパターンというものをよく見かけるようになりそれらを調べてみても全くわからないという状況が続いていました。そこで今回はデザインパターンのうちの一つであるStrategy patternを勉強してみたいと思います。
#Strategy Patternとは
Strategy Pattern自体の説明はいろいろなところでされているので割愛しますが、理解を決定づけた説明は数字のリストのソーティングをする際に、リストを作り直すことなくアルゴリズムのみを変更したい場合にこのパターンを使えば戦略の切り替えが容易になるという説明でした。Template PatternやState Patternも一緒に出てくることが多いようです。
#今回作るもの
ターミナル上に文字を表示させる例が多いですがwinformsでも同じことをしたいと思ったため、今回は画像をピクチャボックスの上に表示させてボタンで操作して遊べるものを作ります。picボタンを押すと移動させることのできる画像の切り替えができ、右下のボタンで移動方向を変更することができます。
ところどころ気になる箇所はありますが今回の目的はデザインパターンを使ってみたいというものなので多めに見てください。
UML図の書き方がよくわかっていないので適当ですがこのようなものを意識して作ります。
#まずはべた書き
書いてみないと始まらないためとりあえず書いてみましょう。ボタンとピクチャボックス等適当に配置します。
初めに、一枚の画像を動かせるようにします。
まずは数値の初期設定をして、ボタンを実装します。上下左右のクリックで移動方向の変更、NumericUpDownで速度の変更をします。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private Image img1;
private float x = 0;
private float y = 0;
private float dx;
private float dy;
float speed = 0.4f;
private void Form1_Load(object sender, EventArgs e)
{
Image imgload1 = System.Drawing.Image.FromFile(@"C:\Users\Fridge\Desktop\cat2.jpg");
img1 = ResizeImage(imgload1, 200, 200);
numericUpDown1.Value = Convert.ToDecimal(speed);
}
private void LeftButton_Click(object sender, EventArgs e)
{
dx = -speed;
dy = 0;
}
private void UpButton_Click(object sender, EventArgs e)
{
dy = -speed;
dx = 0;
}
private void RightButton_Click(object sender, EventArgs e)
{
dx = speed;
dy = 0;
}
private void DownButton_Click(object sender, EventArgs e)
{
dy = speed;
dx = 0;
}
private void StopButton_Click(object sender, EventArgs e)
{
dx = 0;
dy = 0;
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
speed = (float)numericUpDown1.Value;
}
画像をpictureBoxに描画します。Bounceにチェックマークが入っている場合は画像がループせず端で跳ね返ります。
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (img1 != null)
{
e.Graphics.DrawImage(img1,x, y, img1.Width, img1.Height);
if (bounceCheckBox.Checked)
{
x += dx;
if (x >= pictureBox1.Width-img1.Width || x <= 0)
{
dx = -dx;
}
y += dy;
if (y >= pictureBox1.Height-img1.Height || y<=0)
{
dy = -dy;
}
}
else
{
x += dx;
if (x >= pictureBox1.Width)
{
x = -img1.Width;
}
else if (x <= -img1.Width)
{
x = pictureBox1.Width;
}
y += dy;
if (y >= pictureBox1.Height)
{
y = -img1.Height;
}
else if (y <= -img1.Height)
{
y = pictureBox1.Height;
}
}
}
pictureBox1.Invalidate();
LeftButton.Refresh();
RightButton.Refresh();
UpButton.Refresh();
DownButton.Refresh();
}
画像のリサイズです。以降省きます。
private Bitmap ResizeImage(Image img, float maxWidth, float maxHeight)
{
double resizeWidth = img.Width;
double resizeHeight = img.Height;
double aspect = resizeWidth / resizeHeight;
if (resizeWidth > maxWidth)
{
resizeWidth = maxWidth;
resizeHeight = resizeWidth / aspect;
}
if (resizeHeight > maxHeight)
{
aspect = resizeWidth / resizeHeight;
resizeHeight = maxHeight;
resizeWidth = resizeHeight * aspect;
}
Bitmap result = new Bitmap((int)resizeWidth, (int)resizeHeight);
Graphics g = Graphics.FromImage(result);
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.DrawImage(img, 0, 0, result.Width, result.Height);
return result;
}
ここまでは特になんてことない短いコードです。
#仕様変更
今までは画像を一枚表示できればよかったのに急に画像を二枚一度に表示させてください。かつ、画像を一枚一枚動かせるようにもしてくださいと言われました。二枚くらいならそのまま書いてもよさそうですがもしかすると5枚に増えてしまうかもしれません。そこで私はいままでに聞いたことがあるワード「オブジェクト指向」を使えないかと考えます。
Catというクラスを作ってそれにすべて投げ込んでみましょう。動きを表すメソッドMovementも入れてしまいます。
public class Cat
{
public Image Img;
public float X;
public float Y;
public float Dx;
public float Dy;
public int BoundX;
public int BoundY;
public bool Bounce;
public void Movement()
{
if (this.Bounce)
{
this.X += this.Dx;
if (this.X >= this.BoundX - this.Img.Width || this.X <= 0)
{
this.Dx = -this.Dx;
}
this.Y += this.Dy;
if (this.Y >= this.BoundY - this.Img.Height || this.Y <= 0)
{
this.Dy = -this.Dy;
}
}
else
{
X += Dx;
if (X >= this.BoundX)
{
X = -this.Img.Width;
}
else if (X <= -this.Img.Width)
{
X = this.BoundX;
}
Y += Dy;
if (Y >= this.BoundY)
{
Y = -this.Img.Height;
}
else if (Y <= -this.Img.Height)
{
Y = this.BoundY;
}
}
}
}
クラスができたのでそれを使って猫を作ります。
private void Form1_Load(object sender, EventArgs e)
{
Image imgload1 = System.Drawing.Image.FromFile(@"C:\Users\Fridge\Desktop\Tofu.jpg");
img1 = ResizeImage(imgload1, picWidth, picHeight);
Image imgload2 = System.Drawing.Image.FromFile(@"C:\Users\Fridge\Desktop\cat2.jpg");
img2 = ResizeImage(imgload2, picWidth, picHeight);
numericUpDown1.Value = Convert.ToDecimal(speed);
cat1 = new Cat();
cat1.Img = img1;
cat1.BoundX = pictureBox1.Width;
cat1.BoundY = pictureBox1.Height;
cat1.Bounce = false;
cat2 = new Cat();
cat2.Img = img2;
cat2.BoundX = pictureBox1.Width;
cat2.BoundY = pictureBox1.Height;
cat2.Bounce = false;
}
画像が二枚あるのだからcatNumberに値を与え、一つ目の猫が選ばれたときはcat1を動かし、二つ目が選ばれたときはcat2を動かそうと私は考えました。
すべてのボタンでif文が書かれてしまっていますが2枚くらいならこれでも何とかなるでしょう。
int catNumber = 1;
private void LeftButton_Click(object sender, EventArgs e)
{
if (catNumber == 1)
{
cat1.Dx = -speed;
cat1.Dy = 0;
}
else if (catNumber == 2)
{
cat2.Dx = -speed;
cat2.Dy = 0;
}
}
private void UpButton_Click(object sender, EventArgs e)
{
if (catNumber == 1)
{
cat1.Dy = -speed;
cat1.Dx = 0;
}
else if(catNumber == 2)
{
cat2.Dy = -speed;
cat2.Dx = 0;
}
}
private void RightButton_Click(object sender, EventArgs e)
{
if (catNumber == 1)
{
cat1.Dx = speed;
cat1.Dy = 0;
}
else if (catNumber == 2)
{
cat2.Dx = speed;
cat2.Dy = 0;
}
}
private void DownButton_Click(object sender, EventArgs e)
{
if (catNumber == 1)
{
cat1.Dy = speed;
cat1.Dx = 0;
}
else if (catNumber == 2)
{
cat2.Dy = speed;
cat2.Dx = 0;
}
}
private void StopButton_Click(object sender, EventArgs e)
{
if (catNumber == 1)
{
cat1.Dy = 0;
cat1.Dx = 0;
}
else if (catNumber == 2)
{
cat2.Dy = 0;
cat2.Dx = 0;
}
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
speed = (float)numericUpDown1.Value;
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawImage(cat1.Img, cat1.X, cat1.Y, cat1.Img.Width, cat1.Img.Height);
cat1.Movement();
e.Graphics.DrawImage(cat2.Img, cat2.X, cat2.Y, cat2.Img.Width, cat2.Img.Height);
cat2.Movement();
pictureBox1.Invalidate();
LeftButton.Refresh();
RightButton.Refresh();
UpButton.Refresh();
DownButton.Refresh();
}
private void picture1Button_Click(object sender, EventArgs e)
{
catNumber = 1;
}
private void picture2Button_Click(object sender, EventArgs e)
{
catNumber = 2;
}
private void bounceCheckBox_CheckedChanged(object sender, EventArgs e)
{
if (bounceCheckBox.Checked)
{
if (catNumber == 1)
{
cat1.Bounce = true;
}
else if(catNumber ==2)
{
cat2.Bounce = true;
}
}
else
{
if (catNumber == 1)
{
cat1.Bounce = false;
}
else if (catNumber == 2)
{
cat2.Bounce = false;
}
}
}
}
#再び仕様変更
それでは今回は画像を4枚に増やしてください。
少しこじつけ感はありますが、このままでは分岐が多くなってしまうため以下のような分岐を減らすためにstrategy patternに挑戦します。
private void StopButton_Click(object sender, EventArgs e)
{
if (catNumber == 1)
{
cat1.Dy = 0;
cat1.Dx = 0;
}
else if (catNumber == 2)
{
cat2.Dy = 0;
cat2.Dx = 0;
}
else if (catNumber == 3)
{
cat3.Dy = 0;
cat3.Dx = 0;
}
else if (catNumber == 4)
{
cat4.Dy = 0;
cat4.Dx = 0;
}
}
##インターフェイス
まずはインターフェイスに実装するメソッドを定義します。
ここではIStrategyというインターフェイスに上下左右の動き+停止のメソッドと動き続けるのに必要なメソッドMovementを定義しました。
ファイルも分けてCatClassというファイルにインターフェイスとこれから実装するクラスも書いてしまいます。
interface IStrategy
{
void MoveLeft();
void MoveRight();
void MoveUp();
void MoveDown();
void Stop();
void Movement();
}
処理の内容をクラスに記載します。IStrategyを継承しています。
public class Cat : IStrategy
{
public Image Img { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Dx { get; set; }
public float Dy { get; set; }
public int BoundX { get; set; }
public int BoundY { get; set; }
public bool Bounce { get; set; }
public float Speed { get; set; }
public void MoveLeft()
{
this.Dx = -this.Speed;
this.Dy = 0;
}
public void MoveRight()
{
this.Dx = this.Speed;
this.Dy = 0;
}
public void MoveUp()
{
this.Dy = -this.Speed;
this.Dx = 0;
}
public void MoveDown()
{
this.Dy = this.Speed;
this.Dx = 0;
}
public void Stop()
{
this.Dx = 0;
this.Dy = 0;
}
public void Movement()
{
if (this.Bounce)
{
this.X += this.Dx;
if (this.X >= this.BoundX - this.Img.Width || this.X <= 0)
{
this.Dx = -this.Dx;
}
this.Y += this.Dy;
if (this.Y >= this.BoundY - this.Img.Height || this.Y <= 0)
{
this.Dy = -this.Dy;
}
}
else
{
this.X += this.Dx;
if (this.X >= this.BoundX)
{
this.X = -this.Img.Width;
}
else if (this.X <= -this.Img.Width)
{
this.X = this.BoundX;
}
this.Y += this.Dy;
if (this.Y >= this.BoundY)
{
this.Y = -this.Img.Height;
}
else if (this.Y <= -this.Img.Height)
{
this.Y = this.BoundY;
}
}
}
}
実際に処理を行ってくれるクラスStrategistを用意します。このクラスを使い、ボタンにより選択された猫へ切り替えていきます。
クラス名はStrategistにしていますがRunnerでもよいかもしれません。
class Strategist
{
private IStrategy strategy;
public Strategist(IStrategy strategy)
{
this.strategy = strategy;
}
public void ExecuteMovement()
{
strategy.Movement();
}
public void ExecuteMoveLeft()
{
strategy.MoveLeft();
}
public void ExecuteMoveUp()
{
strategy.MoveUp();
}
public void ExecuteMoveDown()
{
strategy.MoveDown();
}
public void ExecuteMoveRight()
{
strategy.MoveRight();
}
public void ExecuteStop()
{
strategy.Stop();
}
public void ChangeStrategy(IStrategy strategy)
{
this.strategy = strategy;
}
}
Form1に戻り実際に使ってみます。画像のロードと猫の作成です。ベースとなるクラスはできているので同じものをnewして実体を複数作ります。
変数に具体的な数字を入れていますがここは省略できる箇所がいくつかあると思います。
private void Form1_Load(object sender, EventArgs e)
{
System.Environment.CurrentDirectory = @"..\..\..\Images";
Console.WriteLine(System.Environment.CurrentDirectory);
Image imgload1 = System.Drawing.Image.FromFile(@"Tofu.jpg");
img1 = ResizeImage(imgload1, picWidth, picHeight);
Image imgload2 = System.Drawing.Image.FromFile(@"cat2.jpg");
img2 = ResizeImage(imgload2, picWidth, picHeight);
Image imgload3 = System.Drawing.Image.FromFile(@"cat3.jpg");
img3 = ResizeImage(imgload3, picWidth, picHeight);
Image imgload4 = System.Drawing.Image.FromFile(@"cat4.jpg");
img4 = ResizeImage(imgload4, picWidth, picHeight);
cat1 = new Cat();
strategist.ChangeStrategy(cat1);
currentCat = cat1;
cat1.Img = img1;
cat1.BoundX = pictureBox1.Width;
cat1.BoundY = pictureBox1.Height;
cat1.Bounce = false;
cat1.Speed = speed;
cat2 = new Cat();
cat2.Img = img2;
cat2.BoundX = pictureBox1.Width;
cat2.BoundY = pictureBox1.Height;
cat2.Bounce = false;
cat2.Speed = speed;
cat3 = new Cat();
cat3.Img = img3;
cat3.BoundX = pictureBox1.Width;
cat3.BoundY = pictureBox1.Height;
cat3.Bounce = false;
cat3.Speed = speed;
cat4 = new Cat();
cat4.Img = img4;
cat4.BoundX = pictureBox1.Width;
cat4.BoundY = pictureBox1.Height;
cat4.Bounce = false;
cat4.Speed = speed;
numericUpDown1.Value = Convert.ToDecimal(speed);
}
ボタンを押すと猫の切り替えができます。ここは分岐をしないといけないので、猫の数に応じて増やさなければいけません。それでも追加のみですでにあるコードを書き換える必要はないので、保守しやすくなったと思います。
private void picture1Button_Click(object sender, EventArgs e)
{
strategist.ChangeStrategy(cat1);
currentCat = cat1;
}
private void picture2Button_Click(object sender, EventArgs e)
{
strategist.ChangeStrategy(cat2);
currentCat = cat2;
}
private void picture3Button_Click(object sender, EventArgs e)
{
strategist.ChangeStrategy(cat3);
currentCat = cat3;
}
private void picture4Button_Click(object sender, EventArgs e)
{
strategist.ChangeStrategy(cat4);
currentCat = cat4;
}
一番大事な上下左右の動きの部分です。picボタンを押すだけで猫が入れ替わるのでそのあとに条件分岐をする必要がなくなりました。これで猫が何匹になってもif文を書く必要がなくなりました。
private void LeftButton_Click(object sender, EventArgs e)
{
strategist.ExecuteMoveLeft();
}
private void UpButton_Click(object sender, EventArgs e)
{
strategist.ExecuteMoveUp();
}
private void RightButton_Click(object sender, EventArgs e)
{
strategist.ExecuteMoveRight();
}
private void DownButton_Click(object sender, EventArgs e)
{
strategist.ExecuteMoveDown();
}
private void StopButton_Click(object sender, EventArgs e)
{
strategist.ExecuteStop();
}
描画部分もすっきりさせることができました。
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawImage(cat1.Img, cat1.X, cat1.Y, cat1.Img.Width, cat1.Img.Height);
e.Graphics.DrawImage(cat2.Img, cat2.X, cat2.Y, cat2.Img.Width, cat2.Img.Height);
e.Graphics.DrawImage(cat3.Img, cat3.X, cat3.Y, cat3.Img.Width, cat3.Img.Height);
e.Graphics.DrawImage(cat4.Img, cat4.X, cat4.Y, cat4.Img.Width, cat4.Img.Height);
strategist.ExecuteMovement();
pictureBox1.Invalidate();
LeftButton.Refresh();
RightButton.Refresh();
UpButton.Refresh();
DownButton.Refresh();
}
最終的に出来上がったものは最初に紹介したこちらになります。picボタンで動かしたい画像を選び、その画像上下左右に動かすことができます。
#さいごに
Strategy Patternを勉強してみました。実際にちゃんと実装できたかはわからないですし、改善点も多いと思います。(この場合においてはStrategy patternを使わなくてもcurrentCatをそのまま使ってしまえばやりたいことができてしまう。) それでも今まで書いていたコードよりはきれいになりオブジェクト指向の理解、実装に役立ったと思います。
すべてのクラス変数にget setしてしまっていますがプロパティはよくわかっていないので今後深く勉強したいです。
ソースコードは需要はないとは思いますが、もしあれば上げようと思います。