Zero-Fat 設計論¶
「不純物ゼロ」の設計思想。コードから冗長性を徹底排除し、成功パスのロジックだけを残す。
コア原則¶
| 原則 | 意味 | 禁止されるもの |
|---|---|---|
| Silent Operator | コードは自己記述的であれ | コメント、Docstring |
| Fail Fast | 予期せぬ事象は即座にクラッシュ | try-catch, try-except |
| Zero-Fat | 未使用コードを徹底削除 | リトライ、タイムアウト、デッドコード |
Ruff ルールの解読¶
ルート pyproject.toml で定義された Ruff ルールの意味を理解する。
select = [
"E", # pycodestyle errors — PEP 8 準拠
"F", # pyflakes — 未使用 import、未定義変数
"B", # flake8-bugbear — よくあるバグパターン
"I", # isort — import の並び順
"N", # pep8-naming — 命名規則
"UP", # pyupgrade — 古い構文の自動更新
"PL", # pylint — コード品質
"SIM", # flake8-simplify — 条件式の簡略化
"ANN", # flake8-annotations — 型アノテーション必須
"ERA", # eradicate — コメントアウトされたコード検出
"T20", # flake8-print — print文の検出
"RET", # flake8-return — return文の最適化
"ARG", # flake8-unused-arguments — 未使用引数
"PTH", # flake8-use-pathlib — os.path → pathlib
"PIE", # flake8-pie — 不要な構文
"RUF", # Ruff固有 — 追加のベストプラクティス
]
特に重要なルール
- ERA: コメントアウトされたコードを「死」として検出する。Zero-Fat の核心。
- ANN: 型ヒントの欠如を許さない。コメントの代わりに型で意図を伝える。
- T20:
printデバッグを許さない。ロガーを使うか、削除する。
実例分析: game_service.py¶
rule-scribe-games/app/services/game_service.py から Zero-Fat パターンを学ぶ。
パターン 1: ホワイトリスト定数¶
_ALLOWED_FIELDS = {
"id", "slug", "title", "title_ja", "title_en",
"description", "summary", "rules_content",
"structured_data", "source_url", "affiliate_urls",
"view_count", "search_count", "data_version",
"is_official", "min_players", "max_players",
"play_time", "min_age", "published_year",
"image_url", "official_url", "bgg_url", "bga_url",
"amazon_url", "audio_url", "created_at", "updated_at",
}
なぜこう書くか: コメントなしで「何が許可されているか」を定数自体が表現する。
変数名 _ALLOWED_FIELDS が自己記述的。セットのイミュータビリティがバリデーションの意図を暗示する。
パターン 2: 早期リターンによる Fail Fast¶
async def update_game_content(self, slug: str, fill_missing_only: bool = False) -> dict[str, object]:
game = await supabase.get_by_slug(slug)
if not game:
return {}
ctx = f"{game.get('title')}: {game.get('summary')}"
result = await generate_metadata(str(game.get("title")), ctx)
merged = _merge_fields(game, result, fill_missing_only)
merged["id"], merged["slug"] = game["id"], slug
merged["data_version"] = int(game.get("data_version", 0) or 0) + 1
out = await supabase.upsert(merged)
return out[0] if out else {}
観察ポイント:
try-exceptが一切ない。supabase.get_by_slugが失敗すれば例外が伝播してクラッシュする。if not game: return {}は唯一の分岐。成功パスが直線的に読める。- 各変数は一度だけ代入され、パイプラインのように流れる。
演習: リファクタリング¶
以下のコードを Zero-Fat 原則に従ってリファクタリングせよ。
Before¶
def get_user_data(user_id: int):
"""ユーザーデータを取得する関数"""
try:
# データベースからユーザーを検索
user = db.find_user(user_id)
if user is None:
print(f"User {user_id} not found") # デバッグ用
return None
# ユーザー名を整形
name = user.name.strip()
# メールアドレスを小文字に
email = user.email.lower()
return {"name": name, "email": email}
except Exception as e:
print(f"Error: {e}")
# return None # 旧実装
return None
After¶
def get_user_data(user_id: int) -> dict[str, str] | None:
user = db.find_user(user_id)
if user is None:
return None
return {"name": user.name.strip(), "email": user.email.lower()}
削除されたもの:
Docstring (1行) + コメント (4行) + try-except (3行) + print (2行) + デッドコード (1行) = 11行 → 4行
チェックリスト¶
- [ ]
ERAルールの意味を説明できる - [ ] 自分のコードで
try-exceptを使っている箇所を特定し、削除の可否を判断できる - [ ] 「コメントを書くべき場面」と「変数名で表現すべき場面」を区別できる
- [ ]
ruff check . --fixをプロジェクトに適用し、結果を解釈できる