Python | OOP:コード分割

Python Python
スポンサーリンク

概要(コード分割は「役割ごとに小さな部品へ分け、見通しと変更を楽にする」)

コード分割は、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}
Python

1箇所で全部やるため、どれかの変更が他を壊しやすく、テストも困難。

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のコード構成を自然に作れるようになります。

タイトルとURLをコピーしました