第9章:Entityの公開API設計(壊されない入口)🔒
この章でやりたいこと 🎯
Entity(今回は Task)を、外側(UIやDBやテストコード)から 雑に壊されない ように設計するよ〜!💪😊
「直接書き換え禁止🙅♀️」「ルールはメソッド経由で守る🛡️」がテーマ!
ちなみに、本日時点の安定版TypeScriptは 5.9.3(npm表記)だよ📌 (npm) (6.0/7.0の動きもあるけど、まだ移行期の話が中心なので、教材は安定版のやり方で固めるのが安全だよ〜)🚧✨ (Microsoft for Developers)
1) 「壊されるEntity」ってどんな状態?😱
たとえば、こんな Task があるとするね👇
// ❌ダメ例:外側から何でもできちゃう
export class Task {
constructor(
public id: string,
public title: string,
public completed: boolean,
) {}
}
これ、UI側でこう書けちゃう:
task.completed = true;
task.title = ""; // 空タイトルも入る😇
task.id = "hacked"; // IDすら変えられる😇
つまり Entityが守るべきルール(不変条件)を、外側が踏み荒らせる のが問題なの🥲💥
2) 壊れないEntityの「公開API」3原則 🛡️✨
ここからが本題だよ〜!💖
原則A:状態は外に見せすぎない 👀❌
- フィールドは
private/#privateにして、外から代入できないようにする🔒 - 外に出すなら getter(読み取り専用) を基本にする📤
原則B:変更はメソッド経由だけ ✅
complete()とかrename()みたいな 意図が分かる動詞API を用意する📝✨- その中で「やっていい?ダメ?」を判定する(=ルールを守る)⚖️
原則C:生成も “入口” を絞る 🚪🔒
new Task(...)を外に開放しない(constructorをprivateにする)Task.create(...)みたいな factory(生成メソッド) を入口にする🌱

3) 今回のTaskで「公開API」を決めよう 🧩✨
ミニTaskアプリだと、外側が欲しい情報はだいたいこれ:
id(識別子)🆔title(表示名)📝isCompleted(完了かどうか)✅complete()(完了にする)🔁✅
そして守りたいルールはこれ:
- タイトルは空白ダメ🙅♀️
- タイトル長すぎダメ(例:100文字まで)📏
- もう完了してるのに
complete()はダメ🙅♀️✅
4) 実装してみよう(Result型で “失敗” も丁寧に返す)⚠️➡️🎁
第10章で「ルール違反の表現」を本格化するけど、今章でも最低限 “壊れない入口” のために、Result で返す形にしちゃうね😊✨
(throwでも作れるけど、初心者は Result の方が追いやすいこと多い!)
4-1) Result型(成功/失敗)を用意 🎁
export type Ok<T> = { ok: true; value: T };
export type Err<E> = { ok: false; error: E };
export type Result<T, E> = Ok<T> | Err<E>;
export const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
export const err = <E>(error: E): Err<E> => ({ ok: false, error });
4-2) ドメインエラー(今は最小)⚠️
export type TaskDomainError =
| { type: "InvalidTitle"; message: string }
| { type: "AlreadyCompleted"; message: string };
4-3) Task Entity本体(公開APIを絞る!)🔒
import { Result, ok, err } from "../shared/result";
import { TaskDomainError } from "./taskDomainError";
type TaskId = string;
type CreateTaskParams = {
id: TaskId;
title: string;
};
type RehydrateTaskParams = {
id: TaskId;
title: string;
completedAt: Date | null;
};
export class Task {
// ✅外側から代入不可にする(本当に隠すなら # を使うのが強いよ)
#id: TaskId;
#title: string;
#completedAt: Date | null;
// ✅外側から new を禁止(生成口は factory に統一)
private constructor(params: RehydrateTaskParams) {
this.#id = params.id;
this.#title = params.title;
this.#completedAt = params.completedAt;
}
// ✅読み取り専用の公開API(getter)
get id(): TaskId {
return this.#id;
}
get title(): string {
return this.#title;
}
get isCompleted(): boolean {
return this.#completedAt !== null;
}
get completedAt(): Date | null {
return this.#completedAt;
}
// ✅作成専用の入口
static create(params: CreateTaskParams): Result<Task, TaskDomainError> {
const normalizedTitle = params.title.trim();
if (normalizedTitle.length === 0) {
return err({ type: "InvalidTitle", message: "タイトルが空だよ🥲" });
}
if (normalizedTitle.length > 100) {
return err({ type: "InvalidTitle", message: "タイトルが長すぎるよ🥲(100文字まで)" });
}
const task = new Task({
id: params.id,
title: normalizedTitle,
completedAt: null,
});
return ok(task);
}
// ✅DBなどから復元する入口(外側都合の“復元”も入口を分ける)
static rehydrate(params: RehydrateTaskParams): Task {
// ここは「DBが正しい前提」で軽く復元するだけ、という割り切りが多いよ。
// (必要ならここでも検証してOK)
return new Task(params);
}
// ✅状態遷移はメソッド経由
complete(at: Date): Result<void, TaskDomainError> {
if (this.#completedAt !== null) {
return err({ type: "AlreadyCompleted", message: "もう完了してるよ〜😇" });
}
this.#completedAt = at;
return ok(undefined);
}
}
ポイントだよ〜👇😊✨
- 外側は
task.title = "..."ができない(壊しにくい!)🔒 complete()の中で「二重完了禁止」を守れる🛡️create()にルールが集まるので、どこから作っても安全✨rehydrate()を用意すると「DB復元」と「新規作成」の意図が混ざらない👍
5) 使う側(UseCase側)のイメージ 🎬
「外側(UseCase)がEntityを正しく扱う」感じはこう👇
const created = Task.create({ id: "t-001", title: userInputTitle });
if (!created.ok) {
// ここは Presenter/Controller 側で外側表現に変換していく(後の章でやるよ)
console.log(created.error.message);
return;
}
const task = created.value;
const completed = task.complete(new Date());
if (!completed.ok) {
console.log(completed.error.message);
return;
}
// task を repository.save(task) みたいに渡す(次の章以降で育てる🌱)
6) よくある設計ミスあるある(先に潰す)🧯😆
❌「setterを生やす」
set title(v: string) { this.#title = v; }
これやると結局外側が好き勝手できる入口になるよ〜🙅♀️💥
“変える” は必ず rename(newTitle) みたいな動詞にして、その中で検証しよ!
❌「constructorをpublicにする」
new Task(...) が解放されると、create() の検証を素通りされがち🥲
入口は絞るのが正義✨
❌「EntityがUI/DBの型を持つ」
TaskEntity { httpStatus: 200 } みたいなのは境界崩壊〜!🚧💥
Entityは業務の言葉だけにするよ😊
7) ミニ課題(5分)📝✨
rename(newTitle) を追加してみてね!💖
条件:
- 空白タイトル禁止
- 100文字まで
- 成功したら
#titleを更新
(できたら “元のタイトルと同じなら何もしない” とかも入れると可愛い👏😊)
8) 理解チェック(1問)✅
Q. Task.create() と Task.rehydrate() を分けるメリットを、1行で言うと?🧠✨
(答え例:「新規作成のルール検証と、DB復元の都合を混ぜないため」)
9) AI相棒🤖✨(コピペ用プロンプト)
用途別にどうぞ〜!
-
設計相談: 「Task Entityの公開APIを、外から壊されないように設計して。
create/rehydrate/completeを想定して、ルール(空タイトル禁止、二重完了禁止)を守る形にして」 -
実装補助: 「TypeScriptで
Result型(ok/err)を使って、Task.create()とtask.complete()を実装して。private constructorと#private fieldsを使って」 -
レビュー: 「このEntity設計、外側からルールを破れる抜け道がないかチェックして。改善案も出して」
次の第10章では、この章でちらっと出た “ルール違反(ドメインエラー)をどう表現するか” を、もっと気持ちよく整理していくよ〜!⚠️➡️✨