1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Fat Controller を避ける Controller 設計|MVCで分ける判断軸【外伝G26】

1
Last updated at Posted at 2026-02-02

連載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の窓口経由で触る形にすると後が軽い

次に読む


連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?