第19章:UI(入力/表示)もI/Oだよ 🖥️🚧✨
この章のゴールはね👇 「画面は“入出力装置”」って割り切って、UIを薄〜くすることだよ😊💖 そうすると、単体テストがラクになって、UI変更にも強くなる〜!💪✨
19-1. UIは「外の世界」=I/Oだよ!🌍🔌
UIって、やってることを分解すると超シンプル👇
- ユーザー入力を受け取る(クリック・文字・選択)🖱️⌨️
- 画面に表示する(ラベル・メッセージ・一覧更新)🖥️
- たまにナビ(画面遷移)🚪
つまり UI は、ファイル/DB/HTTP/時刻と同じく「外部I/O」仲間だよ〜📦↔️🌍✨
19-2. UIにロジック置くと何が起きる?👻💥
UIコード(WinFormsのイベント、WPFのCode-behind、BlazorのUI側)に、判断が増えると…😵💫
- テストがしづらい(ボタン押さないと動かない)😇🔫
- 仕様変更が怖い(画面とルールが絡まる)🕸️
- バグが「画面操作の順番依存」になりがち🌀
- ちょっとした修正が“全体の地雷原”になる💣
なので合言葉はこれ👇 **「UIは薄く、判断は中へ」**📦✨
19-3. “薄いUI”の正体:UIは変換・表示だけ🪄🖥️
UIに残してOKなもの(おすすめ)👇😊
- 入力の受け取り(文字列)✍️
- 画面表示(文字に整形、色、並び、ラベル)🎨
- クリック→処理を呼ぶ(橋渡し)🌉
- 画面遷移(次の画面へ)🚪
UIから追い出したいもの(ここからが“判断”)👇🚫
- 料金計算、割引、会員ランク判定、在庫判定…💰📦
- ifだらけのビジネスルール分岐🌳
- DB/HTTP直呼び🗄️🌐
- MessageBoxをルール層で出す(←UIが混ざる)📛
19-4. いちばん小さい形:Humble Object(UIをアホにする)🧸✨

考え方はこれが最強にわかりやすいよ👇
- UI(View):入力/表示だけ🖥️
- Presenter / ViewModel(UIアダプタ):UIの都合と中身を橋渡し🌉
- UseCase(中身):ルール・判断・手続き📦✨(テストしやすい!)
ざっくり図👇
- 外側:UI(I/O)🖥️
- 中:Presenter/ViewModel(変換役)🧩
- さらに中:UseCase(判断/ルール)🧠💖
19-5. ミニ題材:ポイント計算アプリ(UI→中→表示)🎁🧮✨
仕様(かわいいけど実務っぽいやつ)😊
-
購入金額 amount(円)を入力💴
-
会員ランク rank(Normal / Gold)を選択👑
-
ポイント =
- Normal:1%
- Gold:2%
-
ただし amount は 0以上、上限も適当にチェック(例:10,000,000まで)🚧
ここで大事なのは👇 UIは文字列を受け取って表示するだけ。 ポイント計算ルールは中へ📦✨
19-6. “中身(ルール)”をピュアに作る🧼✨
public enum MemberRank
{
Normal,
Gold
}
public static class PointsRule
{
public static int CalculatePoints(int amountYen, MemberRank rank)
{
if (amountYen < 0) throw new ArgumentOutOfRangeException(nameof(amountYen));
var rate = rank switch
{
MemberRank.Normal => 0.01m,
MemberRank.Gold => 0.02m,
_ => throw new ArgumentOutOfRangeException(nameof(rank))
};
// 端数は切り捨て(例)
return (int)Math.Floor(amountYen * rate);
}
}
✅ これ、UIなしで単体テストできるやつ〜!🧪⚡ (入力が同じなら結果も同じ=ピュア)🌿✨
19-7. UIアダプタ(Presenter)で「文字→型」変換する🌉🔁
UIから来るのはだいたい string だからね😅 それを中で使える形にして、UseCase/Ruleを呼ぶよ✨
View(UI)が満たすインターフェース
public interface IPointsView
{
string AmountText { get; }
string RankText { get; }
void ShowResult(string message);
void ShowError(string message);
}
Presenter(橋渡し)
public sealed class PointsPresenter
{
private readonly IPointsView _view;
public PointsPresenter(IPointsView view)
{
_view = view;
}
public void OnCalculateClicked()
{
// 1) UI入力を読む(I/O)🖥️
var amountText = _view.AmountText;
var rankText = _view.RankText;
// 2) 変換・検証(UI寄り)🧩
if (!int.TryParse(amountText, out var amount))
{
_view.ShowError("金額は数字で入れてね💦");
return;
}
if (amount < 0 || amount > 10_000_000)
{
_view.ShowError("金額の範囲が変だよ〜😵💫");
return;
}
if (!Enum.TryParse<MemberRank>(rankText, out var rank))
{
_view.ShowError("ランクが選ばれてないかも👀");
return;
}
// 3) ルール(中身)を呼ぶ📦✨
var points = PointsRule.CalculatePoints(amount, rank);
// 4) 表示(I/O)🖥️✨
_view.ShowResult($"ポイントは {points} pt だよ〜🎉");
}
}
ここがポイント👇💡
- PresenterはUIのI/Oを扱うけど、判断の中心はRule/UseCaseに寄せる
- UIは
ShowResult/ShowErrorだけやってればOK🖥️✨
19-8. WinForms側(超うすUIの例)🪟🖱️
(フォームが IPointsView を実装するイメージだよ)
public partial class PointsForm : Form, IPointsView
{
private readonly PointsPresenter _presenter;
public PointsForm()
{
InitializeComponent();
_presenter = new PointsPresenter(this);
// ここはUIイベント → presenterへ橋渡しだけ🖱️➡️🌉
btnCalc.Click += (_, _) => _presenter.OnCalculateClicked();
}
public string AmountText => txtAmount.Text;
public string RankText => cmbRank.SelectedItem?.ToString() ?? "";
public void ShowResult(string message) => lblResult.Text = message;
public void ShowError(string message) => MessageBox.Show(message, "入力エラー", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
✅ フォームの中に「計算式」ないよね?😊 これが勝ち筋だよ〜🏆✨
19-9. 単体テスト:UI無しでPresenterもテストできる🎭🧪
フォームを起動しなくてOK!最高!⚡
using Xunit;
public sealed class FakePointsView : IPointsView
{
public string AmountText { get; set; } = "";
public string RankText { get; set; } = "";
public string? Result { get; private set; }
public string? Error { get; private set; }
public void ShowResult(string message) => Result = message;
public void ShowError(string message) => Error = message;
}
public class PointsPresenterTests
{
[Fact]
public void 金額が数字じゃないとエラー()
{
var view = new FakePointsView { AmountText = "abc", RankText = "Gold" };
var presenter = new PointsPresenter(view);
presenter.OnCalculateClicked();
Assert.NotNull(view.Error);
Assert.Null(view.Result);
}
[Fact]
public void 正常なら結果が出る()
{
var view = new FakePointsView { AmountText = "10000", RankText = "Gold" };
var presenter = new PointsPresenter(view);
presenter.OnCalculateClicked();
Assert.Null(view.Error);
Assert.Contains("ポイントは", view.Result);
}
}
これで、UIの見た目を変えても🖥️🎨 中のテストは壊れにくいよ〜!🥳✨
19-10. WPF / MVVM だとどうなる?🧩✨
MVVMでも考え方は同じだよ😊
- View(XAML)=入出力🖥️
- ViewModel=UIアダプタ🌉
- UseCase/Rule=中身📦
ViewModelは INotifyPropertyChanged とかでUIと繋ぐけど、
ビジネスルールはUseCaseへ、がコツだよ〜💖
19-11. UIテスト(E2E)は“少なめ”が正解💡🚦
UI自動化テストって、できるけど重い&壊れやすい😵💫 だからおすすめはこのバランス👇
- ルール/UseCase:単体テスト多め(速い⚡)
- Presenter/ViewModel:単体テストそこそこ🧪
- UI自動化:スモークだけ(起動→主要導線1本)🚬
Web UIなら:Playwright が強いよ🌐🎭
Playwrightは .NET でも使えるし、クロスブラウザ対応でE2E向き! (playwright.dev)
Desktop UIなら:候補はいくつか🪟🤖
WinAppDriver系や、UI Automation ライブラリ(例:FlaUI)もあるよ。FlaUIは .NET 向けUI自動化ライブラリとして継続的にリリースされてる。 (GitHub) (ただしUI自動化は環境差・実行の不安定さが出やすいから、まずは単体テスト優先が安心💖)
19-12. AI(Copilot/Codex)で爆速リファクタ🧠⚡🤖
UIからロジックを剥がす作業、AIがめっちゃ得意だよ〜✨
おすすめプロンプト例📝
- 「このWinFormsのクリックイベントから、Presenterクラスを抽出して。Viewインターフェースも作って」🤖
- 「PointsRuleの単体テスト(境界値含む)をxUnitで生成して」🧪
- 「UIに残すべきもの/追い出すべきものを指摘して、差分パッチ案を出して」🔍
⚠️ ただし、AIがやりがちなミス👇
- ルールの中に
MessageBoxを入れちゃう📛 - Presenterが巨大化して“第2の地獄”になる👻
- 例外/戻り値の境界がぐちゃる(第20章で整えるよ)🚨
19-13. “UIが薄い”かセルフチェック✅👀
- 画面クラス(Form/ページ/コンポーネント)に計算式ある?🧮
- 画面クラスでDB/HTTP呼んでる?🗄️🌐
- ifが増えて読みにくい?🌳
- ルールをUI無しでテストできる?🧪⚡
1つでも「うっ…」ってなったら、UIが太り始めてる合図だよ〜😆🍔
19-14. 実践課題🎓✨(手を動かすと身につく!)
課題A:UI地獄の救出🆘
- 既存のボタンイベントにある「判断(if)」を数えてみて👀
- ルールを
Rule/UseCaseに移動📦 - Presenter(またはViewModel)を作る🌉
- 単体テストを3本以上🧪✨(正常・異常・境界値)
課題B:表示だけUIに残す🎨
- 「ポイントは 1,234 pt」みたいに、表示整形をUI側へ寄せる
- ルールは「数」を返すだけにする(ピュア強化)🌿✨
19章のまとめ🎉💖
- UIは I/O(外の世界) 🖥️🚧
- UIにロジック置くとテストがつらい👻
- UIは薄く、判断は中へ 📦✨
- Presenter / ViewModel は「橋渡し」🌉
- 単体テスト中心+UI自動化はスモーク程度が安定🚦
- .NET 10 + C# 14 は Visual Studio 2026 や .NET 10 SDK で触れるよ(最新の言語機能もこの流れでOK) (Microsoft Learn)
- Windowsの“モダンUI”なら WinUI 3(Windows App SDK)も選択肢として継続アップデート中だよ (Microsoft Learn)
次の第20章はね、ここで出てきた 「エラーってどこで握るの?」「例外はどこまで投げるの?」🚨🤔 をスッキリ整理して、さらにテストしやすくするよ〜✨