第12章:Service層(Controllerを太らせない)🍔➡️🥗
(題材:CampusTodo📚✅)
0) この章でできるようになること🎯✨
- 「Controllerがやること/やらないこと」をスパッと分けられる✂️✨
- Fat Controller(太りすぎController) を回避できる💪😤
- 追加・完了みたいな処理を Service層に移して整理できる🧼🧺
- 次の章(保存・Repository・DI)にスムーズに進める🚀✨
ちなみに、2026-01-14時点の最新は .NET 10 / C# 14 だよ〜🧁(.NET 10の最新SDK更新も2026-01-13付) (Microsoft) Visual Studioも Visual Studio 2026 が案内されてるよ🛠️✨ (Microsoft Learn)
1) まず「Fat Controller」ってなに?😵💫🍔

Controllerが、こんな感じになってくるやつ👇
- コマンド文字列の解析(入力パース)もする🧾
- Todoの追加ロジック(ルール)も書く🧠
- 表示用の整形もする🎨
- エラーメッセージも考える💬
- どんどんifが増えて巨大化😇
結果どうなる?
- 同じロジックがあちこちにコピペされがち📎
- 仕様変更が入ると「どこ直すの?😱」ってなる
- テストしづらい(Controllerは外部入力が多いから)🧪🥲
2) Service層ってなにをするの?🥗✨
一言でいうと…
「アプリとしての処理(ユースケース)」をまとめる場所🧠✨
たとえば CampusTodo のユースケースはこんな感じ👇
- Todoを追加する➕
- Todoを完了にする✅
- Todoを編集する✏️
- Todoを削除する🗑️
- 検索する🔍
この「やりたいこと(ユースケース)」を Serviceが担当して、Controllerは交通整理だけに寄せるのがコツ🚦✨ (アプリケーション層の考え方として、Microsoftのアーキテクチャ資料でも “Application layer” として説明されてるよ) (Microsoft Learn)
3) MVC + Service の形(イメージ図)🗺️✨

✅ 目標の依存の流れ
- Controller → Service → Model
- Controller → View(表示)
- ModelがViewやControllerを知らない(逆流しない)🚫
ざっくり図👇
[User入力] → Controller → Service → Model
↓
View(表示)
4) 「どこに何を書く?」判断基準🚦🧠✨
Controllerに書く✅(交通整理)
- 入力を受け取る(文字列)🧾
- コマンドを振り分ける(routing)🗺️
- 形式チェック(数値か?日付か?)みたいな 薄いチェック🔎
- Serviceを呼ぶ📞
- 結果をViewへ渡す🎁
Serviceに書く✅(ユースケース)
- 「Todoを追加する」等の 手順(作る→保存→整形して返す…)🔁
- 「完了済みなら完了にできない」みたいな 業務寄り判断🧠
- 複数のModel操作をまとめる🧩
Modelに書く✅(守るべきルール・状態)
- TodoItemの不変条件🛡️
- 状態変更(MarkDoneとか)✅
- 「無効な状態を作らない」仕組み💪
Viewに書く✅(見せ方だけ)
- 文字の整形、一覧の見せ方、色(Consoleなら記号)🎨✨
- ロジックは混ぜない🙅♀️
5) 実装してみよう:TodoServiceを作る🧑🍳🥗✨
5-1) フォルダ構成(おすすめ)📁
- Models
- Views
- Controllers
- Services ← ここを追加✨
5-2) Result(成功/失敗)を軽く用意する🎁✨
Controllerに「例外処理地獄」を持ち込みたくないので、まずは軽いResultでいこ〜😌🌸
namespace CampusTodo;
public sealed record AppResult(bool IsSuccess, string Message)
{
public static AppResult Ok(string message = "OKだよ✨") => new(true, message);
public static AppResult Fail(string message) => new(false, message);
}
5-3) TodoService(追加・完了)を作る🥗✅
ここではまだ永続化しないので、いったん List
using System;
using System.Collections.Generic;
using System.Linq;
namespace CampusTodo.Services;
public sealed class TodoService
{
private readonly List<TodoItem> _items;
public TodoService(List<TodoItem> items)
{
_items = items;
}
public AppResult AddTodo(string title, DateOnly? dueDate, int importance)
{
// Model側のルールに乗せるのが理想(TryCreateなど)
if (string.IsNullOrWhiteSpace(title))
return AppResult.Fail("タイトルが空っぽだと登録できないよ🥲");
if (importance is < 1 or > 5)
return AppResult.Fail("重要度は1〜5でお願い🙏✨");
if (dueDate is not null && dueDate.Value < DateOnly.FromDateTime(DateTime.Today))
return AppResult.Fail("期限が過去になってるかも…!日付を見直してね📅💦");
var nextId = (_items.Count == 0) ? 1 : _items.Max(x => x.Id) + 1;
var item = new TodoItem(
id: nextId,
title: title.Trim(),
dueDate: dueDate,
importance: importance
);
_items.Add(item);
return AppResult.Ok($"追加したよ〜🎉:{item.Title}");
}
public AppResult CompleteTodo(int id)
{
var item = _items.FirstOrDefault(x => x.Id == id);
if (item is null)
return AppResult.Fail("その番号のTodoが見つからないよ🥲");
if (item.IsDone)
return AppResult.Fail("それ、もう完了してるよ〜✅✨");
item.MarkDone();
return AppResult.Ok($"完了にしたよ✅:{item.Title}");
}
}
"done 3"の"3"を int にするのはControllerCompleteTodo(3)はService
5-4) Controllerを痩せさせる🚦✨
Controllerは「振り分け&呼び出し」に集中💪
using System;
namespace CampusTodo.Controllers;
public sealed class TodoController
{
private readonly TodoService _service;
private readonly TodoView _view;
public TodoController(TodoService service, TodoView view)
{
_service = service;
_view = view;
}
public void Handle(string input)
{
var parts = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0) return;
var command = parts[0].ToLowerInvariant();
var args = (parts.Length == 2) ? parts[1] : "";
switch (command)
{
case "add":
HandleAdd(args);
break;
case "done":
HandleDone(args);
break;
default:
_view.ShowMessage("コマンドがわからないよ🥲(add / done / list ...)");
break;
}
}
private void HandleAdd(string args)
{
// 例: add レポート作成 | 2026-02-01 | 3
var tokens = args.Split('|', StringSplitOptions.TrimEntries);
var title = tokens.Length >= 1 ? tokens[0] : "";
DateOnly? due = null;
var importance = 3;
if (tokens.Length >= 2 && DateOnly.TryParse(tokens[1], out var d))
due = d;
if (tokens.Length >= 3 && int.TryParse(tokens[2], out var p))
importance = p;
var result = _service.AddTodo(title, due, importance);
_view.ShowResult(result);
}
private void HandleDone(string args)
{
if (!int.TryParse(args, out var id))
{
_view.ShowMessage("done の後は番号を入れてね🥺(例:done 2)");
return;
}
var result = _service.CompleteTodo(id);
_view.ShowResult(result);
}
}
6) ミニ演習:追加/完了をServiceに移す🔁✨(やることリスト✅)
-
Servicesフォルダを作る📁 -
TodoServiceを作る🥗 -
Controllerの
Add/Doneの “中身の処理” を Service へ移す✂️ -
Controllerは「入力パース→Service呼び出し→View表示」だけにする🚦
-
動作確認:
add レポート | 2026-02-01 | 3が追加できる🎉done 1が完了できる✅
7) よくある落とし穴⚠️😵💫
- Serviceが“第二のController”になって太る🍔 → ユースケース単位でメソッドを分けて、長くなったら小さく分割✂️
- Controllerが相変わらず全部知ってる🙅♀️ → 「Serviceに渡す前に文字列処理しすぎてない?」をチェック👀
- ModelのルールがServiceに散らばる🧩💥 → “状態としてダメ”はModel側へ(第9章の気持ち)🛡️✨
8) AI活用(この章向け)🤖💡✨
そのまま丸のみ禁止〜🙅♀️✨(でも相棒としては最強💪)
使いやすいお願い文(コピペOK)📝
- 「このControllerのadd/done処理をService層に移したいです。Controllerは入力パースと結果表示だけにして、Serviceにユースケースを寄せた設計案と差分をください」
- 「Fat Controllerになってる箇所を指摘して、Serviceへ移す優先順位をつけて」
- 「Service/Model/View/Controllerの責務が混ざってないかレビューして、混ざってる行を理由付きで教えて」
AIの回答をチェックする観点🔍✨
- Controllerに 業務判断(完了済み判定とか)が残ってない?
- Serviceが 文字列パースしてない?(それはController寄り)
- Modelの不変条件が守られてる?🛡️
9) 理解度チェック(ミニクイズ)🧠✨
done 3の"3"をintに変換するのはどこ?🤔- 「完了済みをもう一回完了にできない」判断はどこ?🤔
- 「期限を“あと3日”表示」にするのはどこ?🤔
✅答え(目安)
- Controller / 2) Service(or Model寄りでもOK) / 3) ViewModel or View 🎀
次章チラ見せ👀✨
次は 保存(永続化) に入るよ〜💾✨ 今Service層を作っておくと、保存先を足してもControllerが太らなくて最高🥳
「今のCampusTodoのコード構成(Controller/Model/View)貼ってくれたら、あなたの実コードをベースに“どこをServiceに移すか”を一緒に仕分けするよ〜🧁✨