第21章 更新で壊れない①:setterを減らす✂️🔒

この章はひとことで言うと、**「勝手に書き換えられる入口(public set)を減らして、不変条件を守れる更新ルートだけ残す」**回だよ〜!😊🎀 C# 14(.NET 10 / Visual Studio 2026)で触れると便利な小ワザも混ぜつつ進めるね✨ (Microsoft Learn)
0. この章のゴール🏁✨
読み終わったら、次ができるようになるよ😊🛡️
- 「public set;」が危ない理由を説明できる⚠️
- setter を減らして、更新をメソッド経由にできる🚪➡️✅
- 「DTOはゆるく」「ドメインはかたく」を分けられる🎀🏛️
- 永続化(DB)や JSON の都合で setter が必要なときの逃げ道も知ってる🧯
1. なんで public set が危ないの?😱💥
1-1. “壊れた状態”を作れる入口が多すぎる🚪🚪🚪
例えばこんなクラスがあったとするね👇
public class Member
{
public string Email { get; set; } = "";
public int Points { get; set; }
public bool IsActive { get; set; }
}
これ、どこからでもこうできちゃう…💥
member.Email = ""; // 空メール
member.Points = -999; // 負ポイント
member.IsActive = false; // 退会?凍結?よく分からない状態
直す場所が分からなくなるし、レビューでも見落ちやすいの🥲
2. 解決方針はこれだけ!📌✨
2-1. 更新の入口を “メソッド” に寄せる🛡️🚪
- プロパティは 基本 “読めるだけ”(setter を弱める)
- 更新は 意図が分かる名前のメソッドだけにする 例:ChangeEmail / AddPoints / Deactivate みたいに🎀
3. setter削減の基本パターン3つ✂️🔒
パターンA:public set → private set(王道)👑
public class Member
{
public Email Email { get; private set; }
public int Points { get; private set; }
public bool IsActive { get; private set; }
public Member(Email email)
{
Email = email;
Points = 0;
IsActive = true;
}
public void ChangeEmail(Email newEmail)
{
Email = newEmail;
}
public void AddPoints(int add)
{
if (add <= 0) throw new ArgumentOutOfRangeException(nameof(add));
Points += add;
}
public void Deactivate()
{
IsActive = false;
}
}
ポイントはこれ👇😊✨
- 不変条件チェックが “更新メソッドの中” に集まる🧲
- 呼び出し側は「何をしたいか」で読める📖💕
パターンB:作るときだけ入れてOK(init を使う)🧊✨
「生成後に変えたくない」プロパティは「init」も超便利だよ〜! init は 生成時だけセットできて、その後は変更できない仕組みだよ🧷 (Microsoft Learn)
public class Profile
{
public required Email Email { get; init; } // 必須!🎀
public string DisplayName { get; init; } = "";
}
「required」は 初期化必須にできる機能だよ🧷 (Microsoft Learn)
ただし!これは “更新モデル” というより「読み取り用」「DTO寄り」で使うと気持ちいいことが多いよ😊
パターンC:C# 14 の field で “楽して安全”🧼✨
C# 14 には「field」っていう プロパティの裏側フィールドを触れる新機能があるよ〜! 「トリムしたい」とか「nullは入れたくない」みたいな軽い整形に便利✨ (Microsoft Learn)
例えば「セット時に必ず Trim」したいとき👇
public class Person
{
public string Name
{
get;
private set => field = value.Trim();
}
public Person(string name)
{
Name = name; // private set なのでここからだけ通す🎀
}
}
「明示的な backing field を書かなくていい」のが嬉しい〜!🥰
4. “集合”があると setter 地獄が加速する🧺💥
こういうの最悪パターン👇
public class Cart
{
public List<CartItem> Items { get; set; } = new();
public decimal Total { get; set; }
}
呼び出し側が勝手に Items をいじって Total がズレる…😇🧨
4-1. 外には IReadOnlyList、中は List(定番)🛒✨
public class Cart
{
private readonly List<CartItem> _items = new();
public IReadOnlyList<CartItem> Items => _items;
public Money Total { get; private set; } = Money.Zero("JPY");
public void AddItem(ProductId productId, int quantity, Money unitPrice)
{
if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
_items.Add(new CartItem(productId, quantity, unitPrice));
RecalculateTotal();
}
public void RemoveItem(ProductId productId)
{
_items.RemoveAll(x => x.ProductId == productId);
RecalculateTotal();
}
private void RecalculateTotal()
{
Total = _items.Aggregate(Money.Zero("JPY"), (acc, x) => acc + x.Subtotal);
}
}
これで **「更新ルートが Cart の中だけ」**になるから、整合性が守りやすいよ〜!😊🛡️
5. でも現実:DB/JSONの都合で setter 要る時あるよね?🧯😅
ここは安心してOK!ちゃんと逃げ道あるよ🎀
5-1. JSON:private setter でもいけることがある🧩✨
System.Text.Json は JsonInclude を使うと private/internal setter を使える説明があるよ📌 (Microsoft Learn)
(例)
public class MemberDto
{
[System.Text.Json.Serialization.JsonInclude]
public string Email { get; private set; } = "";
}
5-2. DB(EF Core):Backing Field という公式ルートがある🗄️🧱
EF Core は「プロパティじゃなくてフィールドに読み書きさせる」設計ができるよ〜! カプセル化したいときに超大事な仕組み✨ (Microsoft Learn)
6. 実践リファクタ演習🧪✨(setter削減の型を体に覚えよう💪🎀)
演習1:public set 地獄を救う✂️😇
次のクラスを「壊れない」ように直してね👇
public class Member
{
public string Email { get; set; } = "";
public int Points { get; set; }
public bool IsActive { get; set; }
}
お題✅
- Email は「Email VO」に置き換える(Create で検証済みのやつ)📧💎
- 「Points は負にならない」
- 「Deactivate したら points を加算できない」
- setter を消して、更新メソッドに寄せる✂️
仕上がりイメージ(例)✨
public class Member
{
public Email Email { get; private set; }
public int Points { get; private set; }
public bool IsActive { get; private set; }
public Member(Email email)
{
Email = email;
Points = 0;
IsActive = true;
}
public void ChangeEmail(Email newEmail) => Email = newEmail;
public void AddPoints(int add)
{
if (!IsActive) throw new InvalidOperationException("Inactive member can't earn points.");
if (add <= 0) throw new ArgumentOutOfRangeException(nameof(add));
Points += add;
}
public void Deactivate() => IsActive = false;
}
演習2:コレクションを “外から壊せない” にする🧺🔒
「Items を外に List のまま出さない」へ変更してね🛒✨ (Items は IReadOnlyList、更新は Add/Remove メソッドだけ)
演習3:テストで “壊れない” を確認🧪🛡️
xUnit でサクッと確認しよう🎀
using Xunit;
public class MemberTests
{
[Fact]
public void AddPoints_InactiveMember_Throws()
{
var email = Email.Create("a@b.com").Value; // Result想定
var member = new Member(email);
member.Deactivate();
Assert.Throws<InvalidOperationException>(() => member.AddPoints(10));
}
}
7. AI活用プロンプト集🤖✨(そのままコピペOK🎀)
7-1. setter削減の設計案を出させる🛠️
- 「このC#クラスのpublic setを減らして、不変条件が守れる設計に直して。更新は意図が分かるメソッドに寄せて。必要ならprivate setやbacking fieldを使って。」
7-2. “壊れ方” を洗い出させる💥🔍
- 「このモデルで不変条件が壊れる更新パターンを20個列挙して。特にpublic setやListの直接操作で起きるものを多めに。」
7-3. テストケース大量生成🧪✨
- 「このクラスの不変条件を満たす/破る境界値テストをxUnitで10〜20個提案して。」
※ Visual Studio では Copilot が統合されて使える案内もあるよ(無料で一部機能、という説明あり)。(Visual Studio)
8. チェックリスト✅🎀(レビューで超使える!)
- public set が残ってる理由は説明できる?(“必要だから”じゃなくて)🙂
- 更新は「意図が分かる名前のメソッド」経由になってる?🛡️
- コレクションを List のまま外に晒してない?🧺🚫
- 「更新したら整合性がズレる」値(Total 等)が外から触れない?🧾🔒
- 永続化/JSON の事情があるなら、Backing Field / JsonInclude など “公式ルート” を使ってる?🗄️🧩 (Microsoft Learn)
9. 次章へのつながり🔁✨
この章で「更新の入口」を絞れたから、次はさらに一段強い守り💪🎀 第22章:更新メソッドそのものを “入口(境界)” として設計して、検証→適用の順番を型にする…って流れがめちゃ気持ちいいよ〜!😊🛡️
必要なら、今まで作った題材(Cart/Member/Subscriptionなど)に合わせて、第21章の演習コードを「あなたの題材」に寄せた完全版も作るよ🎀✨