Windowsフォームアプリケーションでモジュール強度を高める努力

※記事中のソースコードは擬似的なものです
※記事ではVisualStudioのWindowsフォームアプリケーションを題材にしていますが
　MFCや他のGUIアプリケーション開発でも活用できるかと思います

- - -

　WPFでないWindows Form Applicationでは、`class MainForm : Form`のようなForm継承クラスが1つの画面を表し、
MVCで言うViewとControl…そしてModelとしての機能も、そのクラス内に集約しがちです。

　例えば以下です。
※目と頭に悪いコードですので、内容を詳しく考える必要はありません

　`class Hoge`から取ってきたデータを
ボタンクリックにより検索するフォームのイメージ例
※Hoge.FetchAll()はDBからなんらかのデータ列を配列として取ってくるメソッドとする

```csharp:FooSearchForm.cs
namespace Foo {
  public class FooSearchForm : Form {
    public FooSearchForm() {
      InitializeComponents();
      this.SetDefaultDataGridViewStyle();
      this.LoadDefaultDataGridViewData();
    }

    private void SetDefaultDataGridViewStyle() {
      dgvFoo.AllowUserToAddRows       = false;
      dgvFoo.AllowUserToResizeRows    = false;
      dgvFoo.AllowUserToResizeColumns = false;
    }

    private void LoadDataGridViewData() {
      var foos = Hoge.FetchAll()
        .Where(x => x.Kind == "Neko")
        .Select(new object[] { x.Id, x.Name, x.Kind });
      foreach (var foo in foos) {
        dgvFoo.Rows.Add(foo);
      }
    }

    private void btnSearch_Clicked(object sender, EventArgs e) {
      dgvFoo.Rows.Clear();
      var foos = Hoge.FetchAll()
        .Where(x => x.Id   == int.Parse(txtId.Text)
                 && Regex.IsMatch(txtName.Text, x.Name)
                 && x.Kind == txtKind.Text)
        .Select(new object[] { x.Id, x.Name, x.Kind });
      foreach (var foo in foos) {
        dgvFoo.Rows.Add(foo);
      }
    }
  }
}
```

　見ての通りこのFooSearchFormクラスでは、初期化操作ロジックや検索ロジックが
直に書いてあって、ぱっと見わかりにくいです。

　この例ではコードが小さいので理解できますが、画面に様々な機能や複雑なロジックがあった場合
コード量も多くなって、貴方はきっと思わずスパゲッティを茹でてしまいたくなります。


私はこれに対して、以下の解決案を挙げます。

### 具体的ロジックを他のソースに分離する

FooSearchFormクラスを例に挙げます。

  ここでは`class FooSearchFormModule`を作成し、
FooSearchFormクラスのコードのうち、__ロジック部分をそちらに移します__。

```csharp:FooSearchForm.cs
namespace Foo {
  public class FooSearchForm : Form {
    public FooSearchForm() {
      InitializeComponents();
      dgvFoo.SetDefaultDataGridViewStyle();
      dgvFoo.LoadDefaultDataGridViewData();
    }

    private void btnSearch_Clicked(object sender, EventArgs e) {
      dgvFoo.SearchFooData(int.Parse(txtId.Text), txtName.Text, txtKind.Text);
    }
  }
}
```

```csharp:FooSearchFormModule.cs
namespace Foo.Module {
  public static class FooSearchFormModule {
    public static void SetDefaultDataGridViewStyle(this DataGridView dgvFoo) {
      dgvFoo.AllowUserToAddRows       = false;
      dgvFoo.AllowUserToResizeRows    = false;
      dgvFoo.AllowUserToResizeColumns = false;
    }

    public static void LoadDataGridViewData(this DataGridView dgvFoo) {
      var foos = Hoge.FetchAll()
        .Where(x => x.Kind == "Neko")
        .Select(new object[] { x.Id, x.Name, x.Kind });
      foreach (var foo in foos) {
        dgvFoo.Rows.Add(foo);
      }
    }

    public static void SearchFooData(this DataGridView dgvFoo, int id, string name, string kind) {
      dgvFoo.Rows.Clear();
      var foos = Hoge.FetchAll()
        .Where(x => x.Id   == id
                 && Regex.IsMatch(name, x.Name)
                 && x.Kind == kind)
        .Select(new object[] { x.Id, x.Name, x.Kind });
      foreach (var foo in foos) {
        dgvFoo.Rows.Add(foo);
      }
    }
  }
}
```

　試しにFooSearchFormクラスだけを見てみてください。
ロジックを排除したおかげで、このクラスが「何をしているのか」がわかりやすくなったかと思います。

* 大事なところ（とても）
  - FooSearchFormが処理を抽象的に示している
  - FooSearchFormModuleが__FooSearchFormの__処理の具体的ロジックを持っている


### パラメータをオブジェクトに纏める
　FooSearchFormModule.SearchFooDataメソッドでは、FooSearchFormが受け付けた入力(txtId.Text, txtName.Text, txtKind.Text)
を引数として個別に受け取っています。

これはモジュール結合度のうち***データ結合***といわれており、モジュール結合のうち最も良いとされています。 [参考](https://ja.wikipedia.org/wiki/%E7%B5%90%E5%90%88%E5%BA%A6#.E7.B5.90.E5.90.88.E5.BA.A6.E3.81.AE.E7.A8.8B.E5.BA.A6)

　しかしこのパラメータがもし、{txtId.Text, txtName.Text, txtKind.Text}の3つ以上に増えた場合
FooSearchFormModule.SearchFooDataメソッドの引数にも列挙を追加することになります。

　こんなの見たくないですね！
ショッキングなコードを見たくない方は、暫しの間、目を閉じていてください。 HAHAHA!
`dgvFoo.SearchFooData(int.Parse(txtId.Text), txtFirstName.Text, txtSecondName.Text, txtKind.Text, txtTag.Text, txtTag.Text);`

　このような場合は、単一のオブジェクトにまとめてからデータのやり取りを行いましょう。
FooSearchForm.btnSearch_Clickedメソッドから
FooSearchFormModule.SearchFooDataメソッドの呼び出し部分までを記述します。

```csharp:FooSearchObject.cs
namespace Foo.Data {
  public sealed class FooSearchObject {
    public int    Id   { get; set; }
    public string Name { get; set; }
    public string Kind { get; set; }
  }
}
```

```csharp
private void btnSearch_Clicked(object sender, EventArgs e) {
  var fooSearch = new FooSearchObject() {
      Id   = int.Parse(txtId.Text)
    , Name = txtName.Text
    , Kind = txtKind.Text
  };
  dgvFoo.SearchFooData(fooSearch);
}
```


### あとがき (あとがき)
　これらはWPFアプリケーションを少しだけ書いてみて
その後Windowsフォームアプリケーション作成に戻ってみたところ、思いついたものです。

なのでMVVMにとても影響されているかと思います。 ( されていたら、嬉しい )


　最後に、以下に最終的な3つのクラス全てを載せておきます。

```csharp:FooSearchObject.cs
namespace Foo.Data {
  public sealed class FooSearchObject {
    public int    Id   { get; set; }
    public string Name { get; set; }
    public string Kind { get; set; }
  }
}
```

```csharp:FooSearchForm.cs
namespace Foo {
  public class FooSearchForm : Form {
    public FooSearchForm() {
      InitializeComponents();
      dgvFoo.SetDefaultDataGridViewStyle();
      dgvFoo.LoadDefaultDataGridViewData();
    }

    private void btnSearch_Clicked(object sender, EventArgs e) {
      var fooSearch = new FooSearchObject() {
          Id   = int.Parse(txtId.Text)
        , Name = txtName.Text
        , Kind = txtKind.Text
      };
      dgvFoo.SearchFooData(fooSearch);
    }
  }
}
```

```cs:FooSearchFormModule.cs
namespace Foo.Module {
  public static class FooSearchFormModule {
    public static void SetDefaultDataGridViewStyle(this DataGridView dgvFoo) {
      dgvFoo.AllowUserToAddRows       = false;
      dgvFoo.AllowUserToResizeRows    = false;
      dgvFoo.AllowUserToResizeColumns = false;
    }

    public static void LoadDataGridViewData(this DataGridView dgvFoo) {
      var foos = Hoge.FetchAll()
        .Where(x => x.Kind == "Neko")
        .Select(new object[] { x.Id, x.Name, x.Kind });
      foreach (var foo in foos) {
        dgvFoo.Rows.Add(foo);
      }
    }

    public static void SearchFooData(this DataGridView dgvFoo, FooSearchObject fooSearch) {
      dgvFoo.Rows.Clear();
      var foos = Hoge.FetchAll()
        .Where(x => x.Id   == fooSearch.Id
                 && Regex.IsMatch(fooSearch.Name, x.Name)
                 && x.Kind == fooSearch.Kind)
        .Select(new object[] { x.Id, x.Name, x.Kind });
      foreach (var foo in foos) {
        dgvFoo.Rows.Add(foo);
      }
    }
  }
}
```