概要(コード分割は「役割ごとに小さな部品へ分け、見通しと変更を楽にする」)
コード分割は、1つの巨大ファイルや巨大クラスに詰め込まず、責務ごとにモジュール・パッケージ・クラスへ分ける設計です。目的は読みやすさ、変更の局所化、テスト容易性、再利用性の向上。初心者は「役割の境界を決める」「公開面を最小にする」「ファイル構成で意図を表す」の3点に集中すると失敗が減ります。
基本の考え方(責務境界・公開面・依存の向き)
責務境界を先に決める
コード分割は「何をどこに置くか」を先に決めるのが近道です。入力検証、永続化(DB/ファイル)、ビジネスロジック、外部連携(HTTP)、UIやCLIなどを分け、各領域にクラス・関数を配置します。境界が明確だと、変更理由が混ざらず壊れにくくなります。
公開面(インターフェース)を最小に保つ
外部に見せるのは必要最小限のクラス・関数だけにします。内部実装はモジュール内に閉じ込め、直接触らせない。これで依存が減り、差し替えや変更が安全になります。
依存の向きは「上位が下位のインターフェースへ」
上位(ユースケース)は下位(永続化・外部I/O)の具体実装に依存しないよう、インターフェース(ABC/Protocol)を間に挟みます。これでテストではモックを注入でき、実運用では差し替えが簡単になります。
ファイル構成(実務で使えるシンプルな分割パターン)
例:ユーザー登録の最小構成
app/
__init__.py
domain/
__init__.py
models.py # エンティティ(Userなど)
rules.py # ドメイン規則(バリデーションや計算)
usecases/
__init__.py
register_user.py # ユースケース(フローを定義)
infrastructure/
__init__.py
repo.py # 永続化の実装(DB/メモリ)
mailer.py # 通知の実装(メール/Slack)
interfaces/
__init__.py
repository.py # リポジトリのインターフェース(ABC/Protocol)
notifier.py # 通知のインターフェース
main.py # エントリポイント(組み立て・起動)
- domainは“純粋ロジック”、usecasesは“流れ”、infrastructureは“外界(I/O)”、interfacesは“契約(窓口)”。役割が混ざらないので、変更が局所化されます。
コード例(抜粋)
# interfaces/repository.py
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def save(self, user: dict) -> dict: ...
# interfaces/notifier.py
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def welcome(self, email: str) -> None: ...
Python# infrastructure/repo.py
class MemoryUserRepo:
def __init__(self):
self._store = {}
def save(self, user: dict) -> dict:
uid = str(len(self._store) + 1)
out = {**user, "id": uid}
self._store[uid] = out
return out
# infrastructure/mailer.py
class PrintNotifier:
def welcome(self, email: str) -> None:
print(f"[MAIL] to={email} subject='ようこそ' body='登録完了'")
Python# domain/rules.py
def normalize_user(name: str, email: str) -> dict:
return {"name": (name or "").strip(), "email": (email or "").strip()}
def validate_user(u: dict) -> None:
if not u["name"]:
raise ValueError("名前が空")
if "@" not in u["email"]:
raise ValueError("メールが不正")
Python# usecases/register_user.py
from app.domain.rules import normalize_user, validate_user
class RegisterUser:
def __init__(self, repo, notifier):
self.repo = repo
self.notifier = notifier
def run(self, name: str, email: str) -> dict:
data = normalize_user(name, email)
validate_user(data)
saved = self.repo.save(data)
self.notifier.welcome(saved["email"])
return saved
Python# main.py
from app.usecases.register_user import RegisterUser
from app.infrastructure.repo import MemoryUserRepo
from app.infrastructure.mailer import PrintNotifier
def bootstrap():
repo = MemoryUserRepo()
notifier = PrintNotifier()
return RegisterUser(repo, notifier)
if __name__ == "__main__":
uc = bootstrap()
print(uc.run("Taro", "taro@example.com"))
Pythonこの構成なら、DBや通知の差し替えはinfrastructureだけで完結し、ユースケースはそのまま。テストではMemory版やフェイクを注入できます。
重要ポイントの深掘り(循環依存・import・init.py・テスト)
循環依存を避ける
モジュールAがBをimportし、BがAをimportすると循環で壊れます。共通の契約(インターフェース)を別モジュールに切り出し、両者はそれに依存する形にします。ビジネスロジックは下位実装を知らないのが基本です。
importの指針(絶対importを推奨)
パッケージ内の参照は絶対import(app.domain.modelsのようなフルパス)を使うと、移動やリネームに強くなります。相対importは小規模で便利ですが、規模が増すほど迷いやすくなります。
init.pyの役割(パッケージと公開面)
パッケージにするにはinit.pyが必要です。外部へ見せたいAPIだけをallや再importで整えると、外からの使い方がシンプルになります。内部詳細はそのまま隠すのが安全です。
テストを“分割の品質検査”にする
ドメイン関数はユニットテストで純粋に検証、ユースケースはフェイクリポジトリ・フェイク通知で検証、インフラ層は統合テストで接続確認。層ごとにテストを分けると、分割の妥当性が自然に高まります。
現実の運用(設定駆動・依存性注入・拡張の仕方)
設定駆動にする
本番・開発・テストでインフラ層を切り替える場合は、設定(環境変数や設定ファイル)で「どの実装を使うか」を決めます。レジストリ(名前→クラス)の辞書を用意すると、起動時に選択できます。
依存性注入で「作り方」を外へ
ユースケースは具体クラスを作らず、外から渡してもらいます。ファクトリ(工場)を使えば、作り方の集約と差し替えが同時に達成できます。これでmain.pyだけが“組み立て”の責務を持ち、他の層は不変に保てます。
拡張は層追加・モジュール追加で
新機能が増えたら、usecasesにユースケースを追加、domainに規則やモデルを追加、infrastructureに必要な実装を追加。既存ファイルの肥大化を避け、役割単位で追加するのがコツです。
よくある落とし穴と回避(巨大クラス・神モジュール・漏れた境界)
巨大クラス・神モジュールの誕生
「便利だから全部まとめる」は破滅の入口。変更理由が1つに絞れない塊は分割します。たとえば“ユーザー関係”でも、検証、正規化、永続化、通知は別モジュールへ。
境界を越えた直接依存
ユースケースがinfrastructureの具体に直接依存すると、差し替えが難しくなります。必ずinterfacesの契約を経由して依存させることで、柔軟性を守ります。
モジュール間の命名と意味のブレ
同じ概念を複数名前で呼ぶと迷います。モデル名・メソッド名・戻り値の形を揃え、ドキュメントのひと言(責務の一文)でズレを防ぎます。
例題(コード分割の前後比較で“効果”を体感)
Before:1ファイルで全部
# app.py(悪い例)
import requests
def register(name, email):
# 正規化+検証+保存+通知が混在
name = name.strip(); email = email.strip()
if "@" not in email: raise ValueError("メールが不正")
r = requests.post("https://db.example.com/save", json={"name": name, "email": email})
r.raise_for_status()
uid = r.json()["id"]
print(f"[MAIL] to={email} welcome!")
return {"id": uid, "name": name, "email": email}
Python1箇所で全部やるため、どれかの変更が他を壊しやすく、テストも困難。
After:分割して組み合わせる
# domain/rules.py
def normalize(name, email): return {"name": name.strip(), "email": email.strip()}
def validate(u):
if not u["name"]: raise ValueError("名前が空")
if "@" not in u["email"]: raise ValueError("メールが不正")
# usecases/register.py
class Register:
def __init__(self, repo, notifier):
self.repo = repo; self.notifier = notifier
def run(self, name, email):
u = normalize(name, email); validate(u)
saved = self.repo.save(u); self.notifier.welcome(saved["email"])
return saved
Python変更は局所化し、テストではrepo/notifierをフェイクに差し替え可能。可読性と安全性が段違いになります。
まとめ(コード分割は“境界・公開面・依存の向き”を決めるだけで効く)
コード分割は、責務ごとにモジュール・パッケージへ分け、公開面を最小にして、依存の向きをインターフェースへ揃える設計です。循環依存を避け、絶対importで見通しを保ち、init.pyでパッケージを整える。テストを層ごとに分け、設定と依存性注入で実行時に差し替え可能にする。これを徹底すれば、初心者でも「読みやすく、壊れにくく、拡張しやすい」OOPのコード構成を自然に作れるようになります。
