2009年5月25日月曜日

DataSetとオブジェクト指向の相性

.Net Frameworkは生産性の高いフレームワークです。ほとんどコーディングをしなくてもバックグラウンドにSQL Server、フロントエンドに.Net Frameworkの本格的なアプリケーションを構築することが可能です。これを支えているのソリューションはおそらくDataSetではないかと思います。ここではDataSetとオブジェクト指向の相性について再考します。

Visual Studioでは開発環境でデータベースへの接続を作成して、その接続からDataSetを作成することができます。さらにDataSetからフォーム上にDataSetと連結したコントロールを配置することができます。この手順には1行のコーディングも必要がありません。

上記の理由から.Net Frameworkは生産性が高いということが言えます。が、落とし穴もじつはあるのでは…と思うわけです。

SQLデータベースをデータストアにしてフロントエンドを開発すると思いがけずオブジェクト指向から外れることがあるように思います。

オブジェクト指向においてクラスは現実のオブジェクトをモデル化する存在といえます。あるモデルを表すためのデータはクラスを中心にして存在するべき存在です。しかし、バックグラウンドにSQLデータベースが存在するとデータはSQLデータベースのテーブルの構造に支配されてしまう感があります。

Visual Studioの開発環境ではフォーム上にいくつかのコントロールが存在して、DataSetにバインドされているアプリケーションが簡単に作成されます。これも有意義なアプリケーションです。しかし、この場合のクラスはフォームであって、フォームにメソッドを作る場合でも現実のオブジェクトを操作する感覚からはかけ離れています。フォームやその上のコントロールのイベントを取得してデータを操作することに終始してしまいます。

これはオブジェクト指向の意図するところではないはずです。

オブジェクト指向は、まず現実のモデル化ありきです。
モデル化に必要なデータはすべてクラスに含めます。そして、実体のあるオブジェクトを操作するかのようにメソッドを実装しなくてはなりません。

DataSetとオブジェクト指向の相性の結論ですが、DataSetはデータの集合です。SQLデータベースのサブセットに相当するのでDataSet=SQLデータベースが存在すると思っても間違いではありません。
DataSetが便利なのは、データの挿入、更新、削除などの操作性の便が図られているからです。もし、SQLデータベースがDataSetと同じ要領で操作できるのならDataSetはそれほど重要ではないかもしれません。

オブジェクト指向のクラスは現実のオブジェクトをモデル化したデータと操作メソッドの集合です。DataSetはデータの集まりですがメソッドを持たせることはできません。DataSetがメソッドを持つことができれば、それも、オブジェクト指向に則することになります。しかし、残念ながらそれはできません。

結局、DataSetはあくまでもSQLデータベースとクラスの仲立ちをするデータベースのサブセットであるということです。

プログラムの実際において実行しなくてはいけないことは、現実のオブジェクトをモデル化したオブジェクトを作成すること。そして、このクラスに必要なデータをDataSetとしてSQLデータベースから切り出して準備して、いちいちクラスにセットすることです。クラスに取り込んだデータは処理後にDataSetにもとした後、更新をかけるという手順を踏むことです。
DataSetのデータをクラスにセットする手順と戻す手順は一度コーディングすれば常に同じ手順なので使いまわしできます。
面倒は増えますがクラスのメソッドの持つ強力さは複雑なソフトで威力を発揮します。
ソフトが複雑になるケースでは、この手間をかけることで全体のコーディングを減らすことができるものと期待できます。

コードの実装はまだしていませんが、今後、実装を試してみたいと思います。

2009年5月23日土曜日

C# 複数フォームから1つのフォームを共有する

C#は.Net Frameworkを含めて生産性の高いツールだと感じます。
ある機能を持った親フォームから子フォームを呼び出して、子フォームのデータを親フォームに返したいことがよくあります。このような場合以下のように実装します。

public partial Class ParentForm : Form
{
    protected string dataFromCoForm;
    private void Button_Click(object sender, EventArgs e)
    {
        ChildForm childForm = new ChildForm(this);
        childForm.Show();
    }
}

public partial Class ChildForm : Form
{
    ParentForm parentForm;
    public ChildForm(ParentForm o)
    {
        parentForm = o;
    }
    public ChildForm_Load()
    {
        parentForm.dataFromCoForm = “親フォームへの文字列”;
    }
}

このコードでは親フォームが子フォームを呼び出す際に自分の参照を渡しています。
子フォームでは受け取った親フォームの参照を親フォーム型のフィールドにコピーしています。
親フォームには子フォームからアクセスできるフィールをがあるため参照を通して親フォームにデータを返すことができます。

ここで他のフォームを作成して同じ子フォームを呼び出そうとすると問題が生じます。
新しく作成したフォームをnewParentFormクラスとした場合、newParentForm型の参照が子フォームに渡されます。子フォームは渡された参照をparentForm型のフィールドへコピーしようとしますが、これは失敗します。(実際にはコンパイル時点でエラーになります)単純に型が違うことが原因です。

子フォームがまったく同じでよいのなら共有するほうが生産性もあがるし、たった1行のために似たような子フォームクラスを作らずに済みます。以下に、このような場合の解決方法を示します。

まず基本的な考え方を説明します。
子フォームを共有する親フォームは、派生元になる中間フォームを作成します。通常親フォームはWindows.Formsクラスから派生しますが、Formsクラスから中間フォームを派生させて、さらに親フォームを派生するように作成します。
中間フォームには子フォームから値を返すフィールドやプロパティ、あるいはメソッドを準備します。
子フォームは親フォームから受け取った参照を中間フォーム型のフィールドにコピーします。親フォームは中間フォームから派生しているためコピーは成功します。
では、以下にコードを示します。

public partial class 中間フォーム : Form
{
    public 中間フォーム()
    {
        InitializeComponent();
    }
    public virtual void SetValue()
    {
    }
}

public partial class 親フォーム1 : 中間フォーム
{
    public 親フォーム1()
    {
        InitializeComponent();
    }
    private void btnCallChildForm_Click(object sender, EventArgs e)
    {
        子フォーム CoForm = new 子フォーム(this);
        CoForm.Show();
    }
    public override void SetValue(string s)
    {
        MessageBox.Show("親フォーム1");
        this.textBox.Text = s;
    }
}

public partial class 親フォーム2 : 中間フォーム
{
    public 親フォーム2()
    {
        InitializeComponent();
    }
    private void btnCallChildForm_Click(object sender, EventArgs e)
    {
        子フォーム CoForm = new 子フォーム(this);
        CoForm.Show();
    }
    public override void SetValue(string s)
    {
        MessageBox.show("親フォーム2");
        this.textBox.Text = s;
    }
}

public partial class 子フォーム : Form
{
    object Obj;
    public 子フォーム()
    {
        InitializeComponent();
    }
    public 子フォーム(object o)
    {
        InitializeComponent();
        Obj = o;
    }
    private void btnReturn_Click(object sender, EventArgs e)
    {
        中間フォーム superForm = (中間フォーム)Obj;
        superForm.SetValue("この値は子フォームからの値です。");
        this.Close();
    }
}

コードは以上です。
2つのフォームから共通のフォームを共有することができています。
中間フォームにはテキストボックスに値をセットするためのメソッドが準備してあります。このメソッドはvirtualメソッドになっていて、2つの親フォームでoverrideしています。中間フォームのメソッドを仮想メソッドにすることで呼び出し元の親フォームのメソッドが実行されるようになります。

この方法で1つのフォームを共有できますが注意点があります。
子フォームはsetValueメソッドで親フォームへデータを返すため、新しく作成する親フォームはsetValueメソッドをオーバーライドする必要があります。
また、今回メソッドで値を親フォームにセットしていますが、これはTextBoxへ値をセットするための手法です。もし、フィールドにデータをセットした場合、親フォームでTextBoxに値をコピーしなくてはならないためメソッドで値をセットしました。

ある程度規模の大きいソフトを作ろうと考えているなら、始から中間フォームを準備しておいたほうがよいかもしれません。