【学習用】ドローアプリのリファクタリング

フォーム間通信のサンプルを含め、様々なポイントでリファクタリングを行います
このサンプルは、基本的なC#スキルとVisualStudioの使い方、またFormアプリの知識が必要となります

リファクタリングとは

リファクタリングは、既存のコードを変更することで、そのコードをより理解しやすく、修正しやすく、保守しやすくすることを指します。

リファクタリングは、コードの動作を変えずに、コードの品質を向上させるために行われます。例えば、重複したコードを削除したり、変数や関数の名前をわかりやすくしたり、コードの構造を整理したりすることがあります。これらの変更により、コードはより明確で読みやすくなり、コードのバグ修正や機能追加が容易になります。

リファクタリングは、プログラムの品質を向上させるために広く採用されています。よりよいコードを書くことで、開発者はコードの保守性、拡張性、信頼性を向上させることができます。

プログラムの動きのイメージ

使われているC#の技術

抽象クラス、プロパティ、継承、ポリモーフィズム、インターフェース、オーバーライド、イベント処理(クラス間通信のイベントやPaintEventArgs)等の技術が使われています

使われている名前空間

System.Drawing:グラフィックスプログラミングのためのライブラリ群
System.Windows.Forms:フォームアプリ作成のためのライブラリ群
の名前空間が使用されています

グラフィックスの描画

フォームのダブルバッファリングを有効にするため、DoubleBufferedプロパティをtrueに設定しています
Paintイベントに登録した処理で、各種図形を生成し、グラフィックスオブジェクトを使用して描画しています。

イベントハンドラの登録

スイベントやパレットのイベントなど、様々なイベントに対して、ラムダ式を使用してイベントハンドラを登録しています。

クラスの継承

Formクラスを継承することで、フォームの基本的な機能を利用しています。また、図形描画の情報管理のため、基底クラスから円、四角形、線描画を派生させています。

クラスのインスタンス化

Circle、Rectangle、Lineなどの各図形クラスをインスタンス化しています

インタフェースの実装

Paletteクラスに定義されたIFigureSelectorインタフェースを実装することで、パレットの各種イベントを処理しています。

イベント駆動型プログラミング

イベントハンドラ、ラムダ式、Windowsフォームの利用、条件演算子の利用などが使われています

列挙型の定義

列挙型は、特定の値のセットを定義する型であり、この場合は図形の種類を表すために使用されています。具体的には、"Circle"、"Rectangle"、"Line"の3つの値を持つ。これらの値は、switch文などで使用することができます

インスタンス同士の通信(リファクタリング前のコードの作り)

インスタンス同士の通信(リファクタリング後のコードの作り)

これは、設計ノウハウとして有名な定石の1つです

使われているデザインパターン(設計のノウハウ)

このコードで使われているデザインパターンは、Observer(オブザーバー)パターンです。このパターンは、オブジェクトの状態変化を監視する観察者を定義し、その状態が変化したときに、観察者に通知を送信することで、オブジェクト間の依存関係を解決することを目的としています。

このコードでは、Form1オブジェクトはPaletteオブジェクトを監視し、Paletteオブジェクトの状態が変化したときに通知を受け取ります。具体的には、Paletteオブジェクトに対するイベントが発生した場合、Form1オブジェクトはそれに応じて状態を更新します。たとえば、パレットのFigureTypeChangedイベントが発生した場合、Form1オブジェクトはtype変数を更新し、図形の種類を変更します。

クラス図

作成コード(コーディングが許されているエリア)

VisualStudioが自動作成する以外のコード部分になります
ファイルの分割を含め、耐障害性の向上、変更の容易性の向上を目指してリファクタリングされています

Form1.cs

using System.Drawing;
using System.Windows.Forms;

namespace DrawApp
{
    public partial class Form1 : Form
    {
        // 開始座標と終点座標を格納するプライベート変数
        private Point startPos;
        private Point endPos;

        // パレットを格納するプライベート変数
        private Palette pallette;

        // ペンの太さ、図形の種類、色を格納するプライベート変数
        private int penSize;
        private FigureType type;
        private Color color = Color.Black;

        public Form1()
        {
            InitializeComponent();

            // ダブルバッファリングを有効にする
            this.DoubleBuffered = true;

            // マウスイベントを登録する
            // マウスが押されたときに開始座標を更新する
            this.MouseDown += (sender, e) => this.startPos = new Point(e.X, e.Y);
            // マウスが移動したときにドラッグしている場合は終点座標を更新して描画を更新する
            this.MouseMove += (sender, e) =>
            {
                if (e.Button == MouseButtons.Left)
                {
                    this.endPos = new Point(e.X, e.Y);
                    this.Invalidate();
                }
            };

            // 各図形を生成する
            Circle circle = new Circle();
            Rectangle rectangle = new Rectangle();
            Line line = new Line();

            this.Paint += (sender, e) =>
            {
                // 現在の図形を作成する
                Figure figure = null;
                Size size = new Size(endPos.X - startPos.X, endPos.Y - startPos.Y);

                // 図形の種類に応じて図形を作成する
                switch (type)
                {
                    case FigureType.Circle:
                        {
                            circle.StartPosition = this.startPos; 
                            circle.Size=size;
                            circle.Color = color;
                            figure = circle;
                            break;
                        }
                    case FigureType.Rectangle:
                        {
                            rectangle.StartPosition= this.startPos;
                            rectangle.Size=size;
                            rectangle.Color = color;
                            figure = rectangle;
                            break;
                        }
                    case FigureType.Line:
                        {
                            line.StartPosition = this.startPos;
                            line.EndPosition = this.endPos;
                            line.PenSize = penSize;
                            line.Color = color;
                            figure = line;
                            break;
                        }
                }

                // 図形を描画する
                figure.Draw(e);
            };

            // パレットを作成し、各イベントを登録する
            this.pallette = new Palette();
            this.pallette.FigureTypeChanged += (sender, type) => this.type = type;
            this.pallette.PenSizeChanged += (sender, penSize) => this.penSize = penSize;
            this.pallette.ColorChanged += (sender, color) => this.color = color;
            this.pallette.Show();
        }
    }
}

このコードは、Windows フォーム上で図形を描画するためのアプリケーションを実現するための C# コードです。

具体的には、以下のような機能が実現されています。

  1. 図形の種類を選択するためのパレットを表示する
  2. 図形を描画するための開始座標と終点座標を保持する
  3. 図形の種類、ペンの太さ、色を保持する
  4. パレットのイベントを受け取って、図形の種類、ペンの太さ、色を変更する
  5. マウスのドラッグによって、図形を描画する

このコードは、Windows フォームアプリケーションとして実装されており、Windows フォームの開発に必要な名前空間である System.DrawingSystem.Windows.Forms を使用しています。また、図形を描画するためのクラスである CircleRectangleLine、およびそれらを抽象化した基底クラスである Figure が宣言されています。これらのクラスは、Windows フォーム上で図形を描画するためのメソッドである Draw メソッドを実装しています。そして、フォーム上にマウスがドラッグされると、選択された図形の種類やペンの太さ、色に応じて、Figure クラスのサブクラスのオブジェクトを作成して、Draw メソッドを呼び出すことで、図形を描画します。

Palleteクラスへのイベントの登録

this.pallette.FigureTypeChanged += (sender, type) => this.type = type;

このコードは、this.palletteオブジェクトのFigureTypeChangedイベントに対して、ラムダ式を使ってイベントハンドラーを登録しています。このイベントハンドラーでは、FigureTypeChangedイベントが発生した際に、type引数に渡された値をthis.typeに代入する処理が行われます。

具体的には、this.palletteオブジェクトがどこかでFigureTypeChangedイベントを発生させると、この登録されたラムダ式が自動的に実行され、type引数に渡された値がthis.typeに代入されます。このように、イベントを利用することで、this.palletteオブジェクトの内部で起こった状態変化を外部で検知し、処理を行うことができます。

イベントが登録されているところ(Palleteクラスのコード)

// 図形の種類が変更されたときに発生するイベント
public event EventHandler<FigureType> FigureTypeChanged;

このコードは、C#言語でイベントを定義するための構文です。 public event キーワードを使用して、EventHandler デリゲート型を定義します。このデリゲート型は、イベントに関連するデータを受け取り、イベントが発生したときに実行されるメソッドを表します。FigureType は、イベントハンドラーによって扱われるデータ型を示します。

また、EventHandler<FigureType> デリゲートを使用して、FigureTypeChanged という名前のイベントを定義しています。イベントを発生させることで、FigureTypeChanged イベントに登録されたすべてのメソッドを呼び出すことができます。

イベントは、オブジェクトの状態が変更されたときに、関連するメソッドを自動的に呼び出すことができます。イベントは、オブジェクト指向プログラミングの重要な概念の一つであり、C#などの多くのプログラミング言語でサポートされています。

// 円形ボタンがクリックされたとき、FigureTypeChangedイベントを発生させる
circleButton.Click += (sender, e) => FigureTypeChanged?.Invoke(this, FigureType.Circle);

このコードは、円の図形選択のボタンがクリックされた場合に、FigureTypeChangedというイベントを発生させるためのコードです。

具体的には、circleButtonという円の図形選択のボタンが存在し、このボタンがクリックされたときに、右辺にあるラムダ式が実行されます。このラムダ式は、FigureTypeChangedイベントを発生させ、イベントの発生元であるthisオブジェクトと、円のタイプを表すFigureType.Circleの列挙型をイベントハンドラに渡します。

FigureTypeChangedイベントが発生することで、他のコードでこのイベントをハンドリングすることができます。たとえば、円のタイプを描画するためのメソッドがある場合には、FigureTypeChangedイベントをハンドリングすることで、このメソッドを呼び出すことができます。

Pallete.cs

using System.Drawing;
using System.Windows.Forms;
using System;

namespace DrawApp
{
    public partial class Palette : Form
    {
        // 図形の種類が変更されたときに発生するイベント
        public event EventHandler<FigureType> FigureTypeChanged;

        // ペンの太さが変更されたときに発生するイベント
        public event EventHandler<int> PenSizeChanged;

        // 色が変更されたときに発生するイベント
        public event EventHandler<Color> ColorChanged;

        public Palette()
        {
            InitializeComponent();

            // 円形ボタンがクリックされたとき、FigureTypeChangedイベントを発生させる
            circleButton.Click += (sender, e) => FigureTypeChanged?.Invoke(this, FigureType.Circle);

            // 四角形ボタンがクリックされたとき、FigureTypeChangedイベントを発生させる
            rectangleButton.Click += (sender, e) => FigureTypeChanged?.Invoke(this, FigureType.Rectangle);

            // 直線ボタンがクリックされたとき、FigureTypeChangedイベントを発生させる
            lineButton.Click += (sender, e) => FigureTypeChanged?.Invoke(this, FigureType.Line);

            // ペンサイズのテキストボックスにテキストが入力されたとき、PenSizeChangedイベントを発生させる
            penSizeBox.TextChanged += (sender, e) => PenSizeChanged?.Invoke(this, int.TryParse(this.penSizeBox.Text, out int size) ? size : 1);
           
            // 色ボタンがクリックされたとき、カラーダイアログを表示し、ColorChangedイベントを発生させる
            colorButton.Click += (sender, e) =>
            {
                var colorDialog = new ColorDialog();
                if (colorDialog.ShowDialog() == DialogResult.OK)
                {
                    colorButton.BackColor = colorDialog.Color;
                    ColorChanged?.Invoke(this, colorDialog.Color);
                }
            };
        }
    }
}

このコードは、Windowsフォームアプリケーションで使用される「Palette」フォームの定義です。このフォームには、図形の種類、ペンの太さ、および色を変更するためのコントロールが含まれています。

このフォームには、次の3つのイベントがあります。

  • 図形の種類が変更されたときに発生するFigureTypeChangedイベント
  • ペンの太さが変更されたときに発生するPenSizeChangedイベント
  • 色が変更されたときに発生するColorChangedイベント

各イベントは、イベントハンドラによって処理されます。

Paletteクラスのコンストラクタで、以下のことが実行されています。

  • circleButton, rectangleButton, lineButton, penSizeBox, colorButtonのクリックまたはテキスト入力イベントが発生した場合に、それぞれFigureTypeChanged, PenSizeChanged, ColorChangedイベントが発生するように設定されています。
  • colorButtonがクリックされた場合、カラーダイアログが表示され、ColorChangedイベントが発生するように設定されています。

また、Paletteフォームには、System.DrawingおよびSystem.Windows.Forms名前空間がusingされています。これらの名前空間には、Windowsフォームアプリケーションで使用されるグラフィックスおよびUIコントロールのクラスが含まれています。

FigureType.cs

// 図形の種類を表す列挙型
public enum FigureType
{
    // 円
    Circle,
    // 長方形
    Rectangle,
    // 直線
    Line
}

このコードは、図形の種類を表す列挙型 FigureType を定義しています。列挙型は、特定の値の集合を表現するデータ型で、定数を列挙することができます。この場合、CircleRectangleLine の3つの定数が定義されています。これらの定数は、それぞれ円、長方形、直線を表しています。この列挙型を使うことで、プログラム中で図形の種類を明確に表現することができ、可読性が向上します。

Figure.cs

using System.Drawing;
using System.Windows.Forms;

namespace DrawApp
{
    // 抽象クラスFigureの定義
    abstract class Figure
    {
        // 図形の始点の座標を取得または設定するプロパティ
        public Point StartPosition { get; set; }
        // 図形の色を取得または設定するプロパティ
        public Color Color { get; set; }
        // 図形のサイズを取得または設定するプロパティ
        abstract public void Draw(PaintEventArgs e);
    }
}

このコードは、Windowsフォームアプリケーションで図形を描画するために使用される抽象クラスFigureを定義しています。Figureクラスには、図形の始点の座標、色を表すプロパティが含まれています。また、図形を描画するために必要なDrawメソッドも含まれています。このクラスは、他の図形クラスを作成するためのベースとして使用されます。抽象クラスであるため、Figureクラス自体はインスタンス化できず、そのサブクラスで具体的な実装をする必要があります。

IPaintable.cs

using System.Drawing;

namespace DrawApp
{
    // 塗りつぶし可能なオブジェクトを定義するインターフェイス
    interface IPaintable
    {
        // サイズを取得または設定するプロパティ
        Size Size { get; set; }
    }
}

このコードは、DrawAppという名前空間にIPaintableというインターフェースを定義しています。

このインターフェースには、Sizeという名前のプロパティが含まれており、getおよびsetアクセサーがあります。このプロパティは、Size型のオブジェクトを返し、それを設定することができます。

このインターフェースは、塗りつぶし可能なオブジェクトの共通の機能を抽象化するために使用されます。つまり、塗りつぶし可能なオブジェクトは、そのサイズを取得および設定することができる必要があります。

例えば、円や四角形といった図形オブジェクトが、このインターフェースを実装することで、DrawAppのコード内で共通的なコードを使い回すことができるようになります。

Rectangle.cs

using System.Drawing;
using System.Windows.Forms;

namespace DrawApp
{
    // 図形クラスの継承元となるFigureクラスを定義
    class Rectangle : Figure
    {
        // コンストラクタ:始点、大きさ、色を受け取り、プロパティに代入
        public Rectangle(Point startPotition, Size size, Color color)
        {
            StartPosition = startPotition;
            Size = size;
            Color = color;
        }

        // 描画メソッドのオーバーライド:描画先のGraphicsオブジェクトに色を指定して長方形を描画
        public override void Draw(PaintEventArgs e)
        {
            e.Graphics.FillRectangle(new SolidBrush(Color), new System.Drawing.Rectangle(StartPosition, Size));
        }
    }
}

Circle.cs

using System.Drawing;
using System.Windows.Forms;

namespace DrawApp
{
    // 円クラス
    class Circle : Figure,IPaintable
    {
        public Circle()
        {
        }

        // コンストラクター
        public Circle(Point startPosition, Size size, Color color)
        {
            // 始点座標
            StartPosition = startPosition;
            // 大きさ
            Size = size;
            // 色
            Color = color;
        }

        public Size Size { get; set; }
        // 描画メソッド
        public override void Draw(PaintEventArgs e)
        {
            // 円を描画
            e.Graphics.FillEllipse(new SolidBrush(Color), new System.Drawing.Rectangle(StartPosition, Size));
        }
    }
}

Line.cs

using System.Drawing;
using System.Windows.Forms;

namespace DrawApp
{
    // Figureクラスを継承するLineクラスを定義する
    class Line : Figure
    {
        // 終了位置を表すPointオブジェクトを取得または設定するプロパティ
        public Point EndPosition { get; set; }

        // 線の太さを表す値を取得または設定するプロパティ
        public float PenSize { get; set; }

        // コンストラクター
        public Line(Point startPosition, Point endPosition, float penSize, Color color)
        {
            StartPosition = startPosition;
            EndPosition = endPosition;
            PenSize = penSize;
            Color = color;
        }

        public Line()
        {
        }

        // Figureクラスの抽象メソッドをオーバーライドする
        // 線を描画する
        public override void Draw(PaintEventArgs e)
        {
            e.Graphics.DrawLine(new Pen(Color, PenSize), StartPosition, EndPosition);
        }
    }
}

LineクラスはFigureクラスを継承しており、直線を描画するためのクラスです。EndPositionプロパティは、線の終点を表すPointオブジェクトを取得または設定するためのプロパティであり、PenSizeプロパティは線の太さを表す値を取得または設定するためのプロパティです。

コンストラクターは、開始位置、終了位置、ペンの太さ、色を指定してLineクラスの新しいインスタンスを初期化します。また、引数なしのコンストラクターもあります。

Drawメソッドは、Figureクラスの抽象メソッドをオーバーライドし、線を描画します。e.Graphics.DrawLineメソッドを使用して、Penオブジェクトを作成して線を描画します。このPenオブジェクトには、PenSizeプロパティで指定された太さと、Colorプロパティで指定された色が設定されています。開始位置と終了位置は、StartPositionプロパティとEndPositionプロパティで指定されます。

VisualStudioデザインコード

Form1.Designer.cs

namespace DrawApp
{
    partial class Form1
    {
        /// <summary>
        /// 必要なデザイナー変数です。
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// 使用中のリソースをすべてクリーンアップします。
        /// </summary>
        /// <param name="disposing">マネージド リソースを破棄する場合は true を指定し、その他の場合は false を指定します。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows フォーム デザイナーで生成されたコード

        /// <summary>
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// </summary>
        private void InitializeComponent()
        {
            this.SuspendLayout();
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(13F, 24F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(1114, 795);
            this.Name = "Form1";
            this.Text = "Form1";
            this.ResumeLayout(false);

        }

        #endregion
    }
}

このコードは、Windowsフォームアプリケーションのフォームのデザインを定義するコードです。

Form1クラスがDrawAppの名前空間に属しており、partialキーワードが付いているため、別のファイルに分割されていることがわかります。このファイルでは、Form1のデザインを生成するためのコードが含まれています。

componentsという名前のSystem.ComponentModel.IContainer型の変数が定義されており、フォームが破棄されたときに使用されます。Disposeメソッドは、disposingパラメータがtrueの場合に、components変数を解放するように設定されています。

InitializeComponentメソッドは、Windowsフォームデザイナーが生成したコードであり、フォームの初期化に必要な設定が含まれています。this.AutoScaleDimensionsプロパティは、フォントの自動スケーリングに使用されます。this.ClientSizeプロパティは、フォームのクライアント領域のサイズを設定します。this.Nameプロパティは、フォームの名前を設定し、this.Textプロパティは、フォームのタイトルを設定します。

#region#endregionで囲まれた部分は、デザイナーが生成したコードの範囲を示すために使用されます。この範囲は、コードエディタで変更しないように注意する必要があります。

Pallete.Designer.cs

namespace DrawApp
{
    partial class Palette
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.circleButton = new System.Windows.Forms.Button();
            this.rectangleButton = new System.Windows.Forms.Button();
            this.lineButton = new System.Windows.Forms.Button();
            this.colorButton = new System.Windows.Forms.Button();
            this.penSizeBox = new System.Windows.Forms.TextBox();
            this.SuspendLayout();
            // 
            // circleButton
            // 
            this.circleButton.Location = new System.Drawing.Point(30, 35);
            this.circleButton.Name = "circleButton";
            this.circleButton.Size = new System.Drawing.Size(167, 156);
            this.circleButton.TabIndex = 0;
            this.circleButton.Text = "Circle";
            this.circleButton.UseVisualStyleBackColor = true;
            // 
            // rectangleButton
            // 
            this.rectangleButton.Location = new System.Drawing.Point(241, 35);
            this.rectangleButton.Name = "rectangleButton";
            this.rectangleButton.Size = new System.Drawing.Size(167, 156);
            this.rectangleButton.TabIndex = 1;
            this.rectangleButton.Text = "Rectangle";
            this.rectangleButton.UseVisualStyleBackColor = true;
            // 
            // lineButton
            // 
            this.lineButton.Location = new System.Drawing.Point(456, 35);
            this.lineButton.Name = "lineButton";
            this.lineButton.Size = new System.Drawing.Size(167, 156);
            this.lineButton.TabIndex = 2;
            this.lineButton.Text = "Line";
            this.lineButton.UseVisualStyleBackColor = true;
            // 
            // colorButton
            // 
            this.colorButton.BackColor = System.Drawing.SystemColors.WindowText;
            this.colorButton.Location = new System.Drawing.Point(665, 66);
            this.colorButton.Name = "colorButton";
            this.colorButton.Size = new System.Drawing.Size(66, 65);
            this.colorButton.TabIndex = 3;
            this.colorButton.UseVisualStyleBackColor = false;
            // 
            // penSizeBox
            // 
            this.penSizeBox.Location = new System.Drawing.Point(665, 160);
            this.penSizeBox.Name = "penSizeBox";
            this.penSizeBox.Size = new System.Drawing.Size(152, 31);
            this.penSizeBox.TabIndex = 4;
            this.penSizeBox.Text = "3";
            // 
            // Pallet
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(13F, 24F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(844, 240);
            this.Controls.Add(this.penSizeBox);
            this.Controls.Add(this.colorButton);
            this.Controls.Add(this.lineButton);
            this.Controls.Add(this.rectangleButton);
            this.Controls.Add(this.circleButton);
            this.Name = "Pallet";
            this.Text = "Pallet";
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.Button circleButton;
        private System.Windows.Forms.Button rectangleButton;
        private System.Windows.Forms.Button lineButton;
        private System.Windows.Forms.Button colorButton;
        private System.Windows.Forms.TextBox penSizeBox;
    }
}

このコードは、DrawAppという名前の名前空間の中にPaletteクラスがあることを示しています。Paletteクラスは、Visual Studioのデザイナーで自動生成された部分クラスであり、必要なデザイナー変数と、リソースのクリーンアップを行うためのDisposeメソッドが含まれています。また、Windows Formsアプリケーションでパレット画面を作成するためのボタン、テキストボックス、およびカラーピッカーボタンを含むフォームデザインが含まれています。具体的には、円形、長方形、直線を描画するための3つのボタン、ペンサイズを選択するためのテキストボックス、そしてカラーピッカーボタンが含まれています。これらの要素は、Windows Formsアプリケーションでグラフィックスを描画するためのツールパレットを提供します。