【C#】クラス設計の難しさを知る

object oriented programming design エディター作成
object oriented programming design

マップエディタの開発後記です。
しばらくはこの関連の記事が続きます。

右にロードしたグラフィックチップの一覧が選択できるようになりました。
これで左側のマップフィールドに選択したマップチップを当てはめる処理に進めるわけなんですが、相も変わらず2週間でようやくこの実装ができたという有様です。
超スローペースですね😅

スコープについて

クラスの分割によってデータの受渡しに苦労しているのです。

私は普段からC++を使ってきたので「グローバル・スコープ」「大域変数」が大嫌いで、これらが目の敵になっています。

そもそも全くそれらを使わないでプログラムやシステムは作れないのですが、変数やメソッド、メンバは常に局所的に定義され、カプセル化されるべきであると私は思っています。
(一般的にもプログラミングにおいて自由にアクセスできる変数はあまり良くないと言われます)

ですがこれらは難しいですね。

局所変数にするという事は、例えばクラスAとクラスBがあり、この両方が使用したい変数aがあれば、変数aはどこに定義されるべきか?
そういう話に必然的になると思います。

変数aをクラスA・B両方で使うなら色々考えられる手段があると思いますが、

  • 変数aを定義したファイルをどこからでもアクセスできるようにする
  • クラスA、Bの分割をそもそもやめる
  • 変数aの代わりになる仕組みを再構築する(プログラムの仕様を変える)

などです。

しかしこれらは、プログラムのセキュリティや保守性(維持管理のしやすさ)を損なう対応例であり、私は否定的な意見をする事でしょう。

なら、どうするの?
って事になるのですが、結論から言うと変数aを公開せず(隠蔽しつつ)、変数aを参照する事です。

はいはい、言葉が破綻してるやん、って事なんですがこれが出来るんです。


一般的に、変数を隠すキーワードがprivate(非公開の)なので、private変数を参照できるようにしたいわけです。

参照できたらprivateじゃなくね?って。

まあそうなんですがf^^;

具体的には、

public class SampleUser
{
    private string _name;
    public string GetName() { return _name; }
}

このようにします。
これは俗に言うゲッタ(Getter)と呼ばれるもので、取得についてはpublic(公開された)キーワードを付けて参照できるようにし、本体の変数はprivateキーワードで隠します。

_nameを弄くる部分を制限するんですね。

そして値をセットする場合においても、この仕組みを使えばメソッド内で禁止値などを制御する事ができるようになるわけです。
これで好き勝手に値が弄くられる心配がなくなります。


ぶっちゃけこの程度の問題ならなんでもなかったんですが、今回行き詰った箇所がクラスを跨ぐイベントハンドラを設定するというものでした。

クラスの構造

まずクラスの構造なんですが、以下のようになってます。

クラスの継承を表す表ではないです。
紛らわしくてすみません^^;

実体は以下のような構造です。

/* MainForm.cs */
public partial class MainForm : Form
{
    private readonly MapBuilder? _mapBuilder = new();
    // ... 省略
}
/* MapBuilder.cs */
internal class MapBuilder
{
    private MapStruct? _mapStruct;
    private ChipLists? _chipLists;

    internal MapBuilder()
    {
        _mapStruct = null;
        _chipLists = null;
    }

    internal bool LoadGraphicChipListFile(ref Panel instance, ref PictureBox select_box)
    {
        using LoadGraphDialog opengraphdlg = new();
        // A valid value is assigned only when the existence check returns true for the file path.
        if (opengraphdlg.ShowDialog() == DialogResult.OK && null != opengraphdlg.Filename)
        {
            this.DestroyGraphicChipList(ref instance);
            _selectedChipBox = select_box;
            _chipLists = new(opengraphdlg.Filename, opengraphdlg.GraphicHeight, opengraphdlg.GraphicWidth);
            return _chipLists.Create(ref instance);
        }
        return false;
    }

    internal void DestroyGraphicChipList(ref Panel instance)
    {
        if (null != _chipLists)
        {
            _chipLists.Drop(ref instance);
        }
    }
    // ... 省略
}
/* ChipLists.cs */
internal class ChipLists
{
    private GraphicListFile? _graphic;
    private readonly int _height;
    private readonly int _width;

    internal ChipLists(string path, int height, int width)
    {
        _graphic = new();
        _graphic.FileOpen(path);
        _height = height;
        _width = width;
    }

    internal bool Create(ref Panel objects)
    {
        if (null != _graphic)
        {
            objects.Controls.Clear();
            TableLayoutPanel? table = _graphic.GetGraphicChipList((uint)_height, (uint)_width);
            if (table != null)
            {
                objects.Controls.Add(table);
                objects.BackColor = SystemColors.ControlLight;
                objects.AutoScroll = true;
                return true;
            }
        }
        return false;
    }

    internal void Drop(ref Panel objects)
    {
        if (null != _graphic)
        {
            objects.Controls.Clear();
            _graphic?.FileClose(_graphic.FilePath);
        }
    }
}
/* GraphicListFile.cs */
internal class GraphicListFile : IUserFileIO
{
    private Image? _image = null;
    public string FilePath { protected set; get; }
    public List? ImageList { get; set; } = null;
    internal GraphicListFile()
    {
        FilePath = string.Empty;
    }
    public void FileOpen(string path)
    {
        // ... 省略
    }
    public void FileClose(string path)
    {
        // ... 省略
    }
    private static TableLayoutPanel GetLayoutPanel(uint rowcount, uint columncount, int larger)
    {
        TableLayoutPanel table = new();
        // ... 省略
        return table;
    }
    private static Button GetButton(uint index, Bitmap bitmap)
    {
        Button button = new();
        // ... 省略
        return button;
    }
    internal TableLayoutPanel? GetGraphicChipList(uint rows, uint cols)
    {
        TableLayoutPanel? table = null;
        if (null != _image && GraphicChipConst.ChipListMaxNum >= rows * cols)
        {
            // ... 省略
            uint counter = 0;
            for (var row = 0; row < table.RowCount; row++)
            {
                for (var col = 0; col < table.ColumnCount; col++)
                {
                    Bitmap bitmap = new(cell_size, cell_size);
                    Graphics graphics = Graphics.FromImage(bitmap);
                    graphics.DrawImage(_image, box, img_rect, GraphicsUnit.Pixel);
                    table.Controls.Add(GetButton(counter, bitmap), col, row);
                    // ... 省略
                }
            }
            // ... 省略
        }
        return table;
    }
}

アプリケーションの右側のパネルのグラフィックチップリストに入るTableLayoutPanel(以下TLPオブジェクト)が "GraphicListFile.cs" のオブジェクトです。
このオブジェクトはファイルを選択すると動的に作成され、TLPオブジェクトの各セルにグラフィックチップの画像が埋め込まれたボタンオブジェクトが格納されます。

このボタンをクリックしたら、アプリケーションの右側のパネルの右上端に置かれている小さなパネルボックスにクリックしたボタンの画像を転記して、マップエディットで使用できる画像にする仕組みです。

つまり下のチップコレクションからグラフィックを選択し、右上に置かれたチップをマップフィールドにポチポチ配置していく。
今回のマップエディタの主機能にあたる部分ですね。

問題なのは、右上端のパネルボックスがチップリスト側からもマップフィールド側からも参照される想定である事です。

それはつまり、この記事の最初に言ったクラスAとクラスBどちらからも参照される変数aに該当するインスタンスが右上端のパネルボックスであるという事です。

今回このパネルボックスはクラスCとして作成する事はしませんでした。
クラスCにして、ゲッタとセッタを設置してやれば他のクラスからアクセスできる隠蔽されたデータになって問題は起きないはずです。

しかしこの事象で私はクラスCを作る案を棄却しました。
それはこの手の話題でクラスをボコボコ作るとクラスだらけになってしまうのを恐れたからです。


イベントハンドラの作成と残課題

では実際どうしたのか?という話ですが、
変更点をいくつか抜粋しますと

/* MapBuilder.cs */
internal class MapBuilder
{
    private MapStruct? _mapStruct;
    private ChipLists? _chipLists;
    private PictureBox? _selectedChipBox;  //! 追加。
    // ... 省略

MapBuilderクラスのメンバに直接、右上端のパネルボックスを持たせることにしました。
MapBuilderはMainFormの名前空間にはいないので、グラフィックチップリストを作成するイベントが実行された段階でフォームからパネルボックスの参照インスタンスをもらうことにしました。

次に、

/* MapBuilder.cs */
    // ... 省略
    private void ChipLists_GraphicChipClick(object? sender, EventArgs e)
    {
        Image image = (Image)sender!;
        if (null != _selectedChipBox)
        {
            _selectedChipBox.Image = image;
        }
    }
}

MapBuilderにイベントハンドラを作成。

ですがちょっと待ってください。
実際にイベントを起こすのは "GraphicListFile" にあるTLPオブジェクトの各セルに設置されているボタンをクリックした時です。

やりたいのは、TLPオブジェクトのセルの中のボタンをクリックしたら、右上端のパネルボックスにクリックしたボタンの画像を転記することです。

しかし "GraphicListFile" ファイルでパネルボックスオブジェクトは参照できません。
なぜならパネルボックスオブジェクトは "MapBuilder" ファイルの中にprivateメンバとして定義されており、これは他のファイルからアクセスできないようになっているためです。

ではどうするのでしょうか?

"GraphicListFile" が "MapBuilder" にアクセスするには、"ChipLists"を経由するか、これとは全く関係ないインターフェースを使わないとだめです。
最初 "ChipLists" に_selectedChipBoxの参照を持たせればいいじゃないかと考えましたが、ChipListsクラスとパネルボックスがフレンドであるかという相関関係はスーパークラスの火種になると直感したためやめました。
そもそもTLPオブジェクトを読み込むためのクラスがChipListsクラスなのに、違う役割の_selectedChipBoxを持たせるのはお角違いですしね・・・(´・ω・`)

スーパークラスはなんでもかんでも対応できる複数の役割を担ったクラスのことですが、これは管理が煩雑になって、何より前プロジェクト(*1)がその末路でメンテナンス不可になりプロジェクトが破綻した経緯がありましたので、それを回避した次第です。

そもそも今回のC#.NETでの再構築の目的がそれですし😅


別ファイルのクラスのイベントを登録するため delegate キーワードを使う

結局のところは、MapBuilderクラスのイベントハンドラをGraphicListFileで発火させるようにデリゲートを使いました。

デリゲートについては以下が詳しいです。もしご存じない方がいればどうぞ↓

デリゲート
概要 デリゲート(delegate: 代表、委譲、委託)とは、メソッドを参照するための型です。 C言語やC++言語の勉強をしたことがある人には、 「デリゲートとは関数ポインターや関数オブジェクトをオブジェクト指向に…

/* GraphicListFile.cs */
internal class GraphicListFile : IUserFileIO
{
    public event GraphicChipClickEventhandler? GraphicChipClick;

    private Button GetButton(uint index, Bitmap bitmap)
    {
        Button button = new();
        // ... 省略
        button.Click += GraphicChipList_Click;
        return button;
    }
    private void GraphicChipList_Click(object? sender, EventArgs e)
    {
        Button button = (Button)sender!;
        GraphicChipClick?.Invoke(button.BackgroundImage, e);
    }
}

public delegate void GraphicChipClickEventhandler(object? sender, EventArgs e);

上記の「public event GraphicChipClickEventhandler? GraphicChipClick;」と書く事で、他のファイルで定義したイベントハンドラを登録できるようになります。
GraphicChipList_Clickイベントの中にある「GraphicChipClick?.Invoke(button.BackgroundImage, e);」でGraphicChipClickに登録されているイベントを間接的に呼び出(コールバック)します。

あとはMapBuilderにGraphicChipClickのイベントを登録します。

/* MapBuilder.cs */
// ... 省略
    internal bool LoadGraphicChipListFile(ref Panel instance, ref PictureBox select_box)
    {
        using LoadGraphDialog opengraphdlg = new();
        // A valid value is assigned only when the existence check returns true for the file path.
        if (opengraphdlg.ShowDialog() == DialogResult.OK && null != opengraphdlg.Filename)
        {
            this.DestroyGraphicChipList(ref instance);
            _selectedChipBox = select_box;
            _chipLists = new(opengraphdlg.Filename, opengraphdlg.GraphicHeight, opengraphdlg.GraphicWidth);
            if (_chipLists.Create(ref instance) && null != _chipLists._graphic)
            {
                _chipLists._graphic.GraphicChipClick += ChipLists_GraphicChipClick;  //! 追加
                return true;
            }
        }
        return false;
    }
// ... 省略

あとはカラクリですが、ChipListsの中にあるGraphicListFileインスタンスはpublicに変更してます。

/* ChipLists.cs */
internal class ChipLists
{
    internal GraphicListFile? _graphic;
    // ... 省略

これで完了。
TLPオブジェクトのどこでもクリックするとクリックしたボタンの背景がパネルボックスに転記されます。

基本的にはpublicなクラスを作らずに、無駄な変数やリソースを増産しないこと。
それがオブジェクト指向のみならず、プログラミングにおいてバグを減らすモットーだと思っています。

結局は、クラス分割もどこかのメンバが限定的に公開状態にならないとどうしようもなく、グローバル変数禁止問題が解決できないのも、グローバルスコープ自体が完全に無くせないシステムの事情によるものはそういう話なのではないかと思われます。

グローバル変数0のプログラムなんて聞いた事ないですよ。実際には😂

次は実際に取得したパネルボックスの画像をマップフィールドに配置するイベントハンドラを実装していきます。
またこちらで報告したいと思います。

コメント

タイトルとURLをコピーしました