連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index
このページで扱うこと
前段: 【外伝】超かんたんアーキテクチャモデル入門 で、MVC / MVP / MVVM の違いを整理した。
このページは 「MVCを使おう!」と決定の直後 に、Controllerが太りやすい場面と、太らせないための分け方をまとめる。
やることはシンプルで、Controllerへ集めたくなった時の逃がし先(置き場所と分割パターン)を先に用意しておく。
Fat Controllerとは何か
一般に Fat Controller は、Controllerの中へ色々入っていって、変更のたびに直す場所が増えていく状態を指す。
入りやすいのは次の3つ。
- ビジネスロジック(判断・分岐・計算・状態遷移)
- 画面状態(選択中、編集中、処理中、途中経過)
- 画面の操作(Enabled/Text/Visible、メッセージ、フォーカスなど)
この状態になると、変更のたびにController側の確認範囲が増えやすい。
ビジネスロジックの例(業務アプリ/自作ツール)
ここで言う ビジネスロジック は、画面の都合ではなく「その業務やツールで、そう決まっていること」。
業務アプリでよくある例
- 合計金額が一定以上なら送料が無料
- 在庫が足りないなら引当できない
- 締め日を過ぎたら変更できない
- ステータスが確定なら編集できない
- 割引率や端数処理のルール(四捨五入/切り捨て/切り上げ)
自作ツールでよくある例
関数電卓、Excel検索、Slack通知のようなツールでも「ツール内ルール」が増えると同じことが起きる。
- 入力の整形ルール(全角/半角、空白除去、正規表現、単位変換)
- 検索条件の解釈(AND/OR、部分一致、大小文字、対象列の優先順位)
- 例外の扱い(見つからない時、タイムアウト時、リトライ回数)
- 通知のルール(通知先、抑制条件、本文テンプレ、添付やメンションの扱い)
- 計算のルール(四捨五入、指数/桁数、関数の対応範囲、エラー表示)
これらがControllerへ入り始めると、機能追加のたびにControllerが育ちやすい。
最初に切っておくディレクトリ階層
Controllerが太るのは「置く場所がわからない」まま増えることが多い。
最初にディレクトリを切るだけで、追加が分散しやすくなる。
WinFormsのMVCなら、最初はこれで十分。
/App
/Views
UserEditForm.cs
IUserEditView.cs
/Controllers
UserEditController.cs
/UseCases
SaveUserUseCase.cs
SearchUsersUseCase.cs
/Models
User.cs
/Repositories
IUserRepository.cs
UserRepository.cs
/ViewStates
UserEditViewState.cs
- Views:入力と表示(コントロール操作はここ)
- Controllers:入力を受け、処理を呼び、結果を返す
- UseCases:要求単位の流れ(分岐、順序、入力の扱い)
- Models:業務/ツールとして意味が通る形(計算・状態遷移)
- ViewStates:画面へ返す結果(Message/CanSave/Itemsなど)
- Repositories:DBや外部I/O
※ UseCaseは呼び名。Serviceでも同じ役割として扱える。ここでは「Controllerから逃がす先」としてUseCaseと呼ぶ。
Controllerと画面の数え方(1対1 / 1対N)
WinFormsで最初に選びやすいのは 1画面 = 1Controller。
ただし、画面が育つとそれだけでは足りなくなる場面が出る。
そのため、最初から「1画面の中で分けるパターン」も併記しておく。
基本:1画面 = 1Controller
- 画面の入力と表示が分かれている
- 画面ごとに処理の始まりが分かれる(保存、検索、更新など)
- 変更が画面単位で入る
分割:1画面 = 複数Controller(ブロック分割)
1画面の中に「検索」「一覧」「編集」「集計」のような塊が増えた時に効く。
1対1のまま押し込むより、Controllerをブロック単位で分けた方が太りにくい。
/Controllers
/UserEdit
UserEditController.cs // 画面の起点(配線)だけ
UserEditSearchController.cs // 検索ブロック
UserEditGridController.cs // 一覧ブロック
UserEditSaveController.cs // 保存ブロック
- 起点:どのブロックへ渡すかを決める
- ブロック担当:入力を受け、UseCaseを呼び、結果を返す
1対1のままだと苦しくなる合図
次が出始めたら、1対1のまま抱え込むより分割を考えた方が安全。
- Controllerが長くなり、画面の話と業務の話が同じ場所に増え始めた
-
if/switchが増え続けて、条件の追加が怖くなってきた - 画面の都合(活性/非活性やフォーカス移動)がController側へ増え始めた
- 同じ処理(検索/保存)が別画面からも呼ばれ始めた
この合図が出た状態で「1画面=1Controller」を守ろうとすると、結果的に太りやすい。
Controllerに残す仕事は「受けて呼んで返す」
Controllerは何でも書けてしまうので、枠を決めた方が迷わなくなる。
Controllerに残すのは次の3つ。
- 画面から入力を受け取る
- UseCaseを呼ぶ
- 結果を画面へ返す
この枠を超える話が出たら、他の置き場所(UseCases / ViewStates / Models / Repositories)へ逃がす。
Controllerにコントロールを置く話(基本方針/置く場合の範囲)
基本は、Controllerへコントロール(TextBox/Button/DataGridViewなど)を置かない方が楽。
置き始めると、画面都合の変更がControllerへ集まりやすい。
基本方針(おすすめ)
- Controllerは View(を表すインターフェイス)だけを持つ
- Controllerは Viewが用意したメソッドだけを呼ぶ
-
GetInput()/Apply(state)/ShowError(msg)など
-
- コントロール名をControllerへ出さない
置く場合(現実のやり方)
- DataGridViewの行操作など、表示の都合が強い処理に絞る
- 業務条件に合わせてEnabledを大量に切り替えるなどは避ける
- コントロールを渡す代わりに、必要な操作だけをViewのメソッドにする
-
GetSelectedId()/SetRows(items)/ShowRowError(...)
-
MVCの流れ
WinFormsサンプル
ここで見たいのは、Controllerが“入口”として動いていて、重い処理が外へ逃げている状態。
Controllerが分岐やコントロール操作を抱えない並びが、短いコードで見えると安心しやすい。
// 画面へ返す結果(画面がどうなるかの材料)
public sealed class UserEditViewState
{
public bool CanSave { get; init; }
public string Message { get; init; } = "";
}
// Viewが用意するやり取りの窓口
// Controllerはコントロール名を知らずに済む
public interface IUserEditView
{
string UserName { get; }
int Age { get; }
void Apply(UserEditViewState state);
}
// UseCase: 分岐と順序をまとめる(Controllerへ置きたくなる所を引き受ける)
public sealed class SaveUserUseCase
{
private readonly IUserRepository _repo;
public SaveUserUseCase(IUserRepository repo)
{
_repo = repo;
}
public UserEditViewState Execute(string name, int age)
{
if (string.IsNullOrWhiteSpace(name))
{
return new UserEditViewState { CanSave = true, Message = "名前が空になっている" };
}
if (age < 0)
{
return new UserEditViewState { CanSave = true, Message = "年齢が不正になっている" };
}
var user = User.Create(name, age);
_repo.Save(user);
return new UserEditViewState { CanSave = true, Message = "保存が完了した" };
}
}
// Controller: 受けて呼んで返す
public sealed class UserEditController
{
private readonly IUserEditView _view;
private readonly SaveUserUseCase _useCase;
public UserEditController(IUserEditView view, SaveUserUseCase useCase)
{
_view = view;
_useCase = useCase;
}
public void SaveUser()
{
var state = _useCase.Execute(_view.UserName, _view.Age);
_view.Apply(state);
}
}
// Model: 画面が無くても意味が通る形を保つ
public sealed class User
{
public string Name { get; }
public int Age { get; }
private User(string name, int age)
{
Name = name;
Age = age;
}
public static User Create(string name, int age)
{
return new User(name.Trim(), age);
}
}
// Repository: 永続化の詳細を隠す
public interface IUserRepository
{
void Save(User user);
}
Fat Controller化を止めるポイント
重いルールより「Controllerに増えたものの行き先」を決めておく方が運用しやすい。
見るのは2点だけ。
- Controllers配下で
ifが増えてきたら、UseCases側へ出すと止まりやすい - Controllers配下にコントロール操作が入ってきたら、ViewStateで返す方へ戻すと止まりやすい
まとめ
- Fat Controllerは、ビジネスロジック・画面状態・画面の操作がControllerへ集まると起きやすい
- 置き場所(Views/Controllers/UseCases/ViewStates/Models)を先に切ると、追加が分散しやすい
- 1画面=1Controllerで始めつつ、1画面=複数Controller(ブロック分割)も用意しておくと押し込みが減る
- コントロールは基本Controllerに置かず、Viewの窓口経由で触る形にすると後が軽い
次に読む
- 前段: G25 【外伝】超かんたんアーキテクチャモデル入門──開発前に“役割の分け方”を意識する MVC / MVP / MVVM
- 次回予定
- G27: 超かんたんMVC(巨大Modelの混入点)
- G28: 超かんたんMVC(Viewの役割の話)
- G29: 超かんたんMVVM
連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index