スポンサーリンク

Pythonの境界設計とは?I/Oとロジック分離で“変更に強い”コードにする方法

クラス設計・OOP入門
  1. はじめに
  2. なぜPythonでも「境界設計」が必要なのか
    1. 変更が連鎖するコードの典型例
    2. フレームワーク依存が強すぎる問題
    3. テストが書けない=設計が苦しいサイン
  3. 境界設計の思想的な原点(考え方を先に押さえる)
    1. 関心の分離(SoC)というシンプルだけど強力な考え方
    2. 依存関係の向きがコードの寿命を決める
    3. 「フレームワーク中心」から「ユースケース中心」へ
  4. Python的に理解する「境界」の正体
    1. よく語られる4つのレイヤー
    2. Pythonでは「厳密に分けすぎない」が正解
    3. 境界が曖昧なコードのサイン
  5. 実装で効く:I/Oとロジックを分離するディレクトリ設計
    1. よくあるBefore:すべてが混ざった構成
    2. After:境界を意識したシンプルな分割
    3. ディレクトリごとの役割を整理する
    4. 「最初から完璧」を目指さなくていい
  6. 抽象化の要:Protocol / ABCで境界を作る
    1. なぜ抽象がないと境界が壊れるのか
    2. ABCで作る明示的な契約
    3. Protocolで作るPythonらしい境界
    4. ABCとProtocol、どちらを使うべき?
  7. 依存性注入(DI)で境界を“動かす”
    1. newしないだけで設計は一段よくなる
    2. コンストラクタDIの基本形
    3. エントリーポイントで依存を組み立てる
    4. 手動DIで十分なケースは多い
    5. それでも辛くなったらDIコンテナ
  8. 境界設計 × テスト:なぜテストが一気に楽になるのか
    1. 境界がないとテストが重くなる
    2. 境界があるとテスト対象が一気に小さくなる
    3. モックが最小限で済む理由
    4. 「テストしづらさ」は設計の警告灯
  9. 境界設計とドメイン思考の相性
    1. Entityを「データ入れ物」で終わらせない
    2. ユースケースは「業務の流れ」を語る場所
    3. CRUD地獄から抜け出すヒント
  10. まとめ
    1. この記事で伝えたかったポイント
    2. 私の実務での実感
    3. 次にやるならこの一歩
    4. あわせて読みたい
    5. 参考文献
  11. よくある質問(Q&A)
    1. 関連投稿:

はじめに

Pythonで開発していると、最初はスッキリ書けていたはずのコードが、機能追加を重ねるうちにどんどん触りづらくなっていく……そんな経験はありませんか?

ちょっとした仕様変更のつもりが、API、DB、画面表示まで一緒に修正することになったり、テストを書こうとしたらデータベースや外部APIの準備が必要になったり。「なんでこんなに大変なんだろう?」と感じたことがある人は少なくないと思います。

その原因の多くは、I/O処理(DB・API・UIなど)とビジネスロジックが密結合していることにあります。 ロジックの中に外部とのやり取りが混ざっていると、コードは一気に壊れやすく、変更に弱い構造になってしまうんです。

そこで登場するのが「境界設計」という考え方です。 境界設計は、I/Oとロジックを明確に分け、依存関係の向きを整理することで、コードを「変更しやすく」「テストしやすく」する設計アプローチです。

この記事では、Clean Architectureなどで語られる境界の考え方をベースにしつつ、Pythonの実務で無理なく使える形に落とし込んで解説していきます。 難しい理論を振りかざすのではなく、「なぜ必要なのか」「どこから手を付ければいいのか」が自然と理解できる構成にしています。

設計に自信がない人も、すでにコードが複雑になって悩んでいる人も、読み終わる頃には「まずここを分ければいいんだ」という一歩が見えるはずです🙂 それでは、Pythonの境界設計の世界を一緒に見ていきましょう。




なぜPythonでも「境界設計」が必要なのか

境界設計というと、「大規模システム向け」「意識高い設計」というイメージを持たれがちですが、 実はPythonのような軽量な言語ほど、その恩恵を受けやすいと私は感じています。

理由はシンプルで、Pythonは書こうと思えば何でもすぐ書けてしまうからです。 DBアクセスもAPI呼び出しも、ビジネスロジックも、同じ関数・同じファイルに自然と集まっていきます。

変更が連鎖するコードの典型例

たとえば、次のような流れで開発が進むことはよくあります。

  • 最初は1画面・1APIだけのシンプルな処理
  • 仕様追加で条件分岐が増える
  • DB構造変更に合わせてロジックも修正
  • 画面表示の都合でロジックにif文が増殖

この状態では、「価格計算を少し変えたい」だけなのに、 API、DB、画面、テストコードまで同時に触る必要が出てきます。
変更の影響範囲が読めず、修正が怖くなっていくんですよね。

フレームワーク依存が強すぎる問題

DjangoやFastAPIはとても便利ですが、便利すぎるがゆえに、 ビジネスロジックがフレームワークの中に溶け込んでしまいがちです。

その結果、

  • 別のフレームワークへ移行しづらい
  • CLIやバッチ処理で再利用できない
  • フレームワーク込みでないとテストできない

といった「身動きの取れないコード」になってしまいます。

テストが書けない=設計が苦しいサイン

「テストを書くのが面倒」というより、 テストを書くための準備が重すぎる状態になっていませんか?

ロジックの中にI/O処理が混ざっていると、 テストのたびにDB接続、外部API、設定ファイルの準備が必要になります。 これでは、テストを書く気力が削られてしまいます。

境界設計は、この問題を構造そのものから解決する考え方です。 ロジックをI/Oから切り離すことで、変更にもテストにも強いコードへと変えていけます。

次の章では、その境界設計がどんな思想をベースにしているのか、 「考え方」の部分から整理していきます。




境界設計の思想的な原点(考え方を先に押さえる)

境界設計をうまく使いこなすために大切なのは、 いきなりディレクトリ構成やコード例を見ることではありません。
まずは「なぜその分け方をするのか」という思想を押さえておくことが重要です。

この考え方の土台になっているのが、Clean Architecture や SOLID 原則で語られる設計思想です。 Python特有の話に入る前に、共通するエッセンスだけを整理しておきましょう。

関心の分離(SoC)というシンプルだけど強力な考え方

境界設計の出発点は、とてもシンプルです。

「同じ理由で変更されるものはまとめ、違う理由で変更されるものは分ける」

価格計算のルールと、DBへの保存方法は、変更される理由がまったく違います。 にもかかわらず同じ場所に書いてしまうと、 本来関係ないはずの修正が連鎖していきます。

依存関係の向きがコードの寿命を決める

境界設計でもっとも重要なのが、依存関係の向きです。

原則はひとつだけ。

依存は常に「外側」から「内側」へ向ける

ここでいう内側とは、ビジネスルールやユースケースなど、 アプリケーションの「本質」にあたる部分です。
逆に、フレームワーク、DB、外部API、UIといったものは外側の存在になります。

内側のコードが外側を知らなければ、 DBをPostgreSQLからSQLiteに変えても、 Web APIをCLIツールに置き換えても、 ビジネスロジックはそのまま生き残れます。

「フレームワーク中心」から「ユースケース中心」へ

多くのプロジェクトでは、 「Djangoアプリ」「FastAPIプロジェクト」という意識から設計が始まります。

でも境界設計が目指すのは、 「何をするシステムなのか」が構造から伝わる状態です。

たとえば、

  • 注文を確定する
  • ユーザーを登録する
  • 支払いを検証する

こうしたユースケースが主役で、 フレームワークはあくまで「実行するための道具」に過ぎません。

この考え方を体系的にまとめたのが、次の一冊です。

Clean Architecture 達人に学ぶソフトウェアの構造と設計
Amazonでチェックする| ✅ 楽天でチェックする

正直、すべてをそのままPythonに当てはめる必要はありません。 ただ、「なぜ境界が必要なのか」「なぜ依存の向きを守るのか」を理解するうえで、 これ以上に分かりやすい原典はないと私は思います。

次の章では、これらの思想を踏まえたうえで、
Python的に境界をどう捉え、どう分けるかを具体的に見てい




Python的に理解する「境界」の正体

ここまでで、境界設計の思想的な背景は押さえられたと思います。 ただ、このままだと「で、Pythonでは結局どう分ければいいの?」という疑問が残りますよね。

この章では、Clean Architectureで語られる層構造をベースにしつつ、 Pythonの現場で無理なく使える形に翻訳していきます。

よく語られる4つのレイヤー

境界設計では、システムを同心円状のレイヤーとして捉えます。 名前だけ見ると難しそうですが、役割はとてもシンプルです。

  • Entities(エンティティ)
    ビジネスのルールそのもの。 価格計算や状態遷移など、「会社として変えにくいルール」を持つ。
  • Use Cases(ユースケース)
    アプリケーションとして何をするか。 エンティティをどう使うかを定義する“司令塔”。
  • Interface Adapters(境界・アダプター)
    Web、DB、外部APIとの橋渡し役。 データ形式の変換や呼び出しの仲介を担当。
  • Frameworks & Drivers(外側の世界)
    Django、FastAPI、ORM、DB、UIなど、変化しやすい技術要素。

重要なのは、この分類を厳密に守ることではありません。 大切なのは「どこが変わりやすく、どこが守るべき中核なのか」を意識することです。

Pythonでは「厳密に分けすぎない」が正解

PythonでClean Architectureをそのまま再現しようとすると、 ファイルやクラスが増えすぎて逆に分かりづらくなることがあります。

そこでおすすめなのが、次のような割り切りです。

  • エンティティとユースケースは「ロジック層」としてまとめてもOK
  • 重要なのは「I/Oを含まない純粋なコード」が存在すること
  • フレームワークのコードがロジックを支配しない構造にする

つまり、完全な層構造よりも、依存の向きが守られているかが判断基準になります。

境界が曖昧なコードのサイン

自分のコードに境界があるかどうかは、次の質問でチェックできます。

  • この処理、DBやAPIなしでテストできる?
  • Web以外(CLI・バッチ)から再利用できる?
  • フレームワークを外したら何も残らない?

もし「全部つらい…」と感じたら、 それは境界がまだ曖昧なサインです。

次の章では、こうした考え方を踏まえて、 実際にどうディレクトリを分け、構造として境界を作るのかを 具体例と一緒に見ていきます。




実装で効く:I/Oとロジックを分離するディレクトリ設計

境界設計は思想だけ分かっていても、実装に落とせなければ意味がありません。 この章では、Pythonで現実的に運用しやすいディレクトリ構成を例に、 I/Oとロジックをどう分けるかを見ていきます。

よくあるBefore:すべてが混ざった構成

境界を意識せずに成長したプロジェクトでは、 次のような構成になっていることが多いです。

app/
├─ main.py
├─ api.py
├─ models.py
├─ services.py
└─ db.py

一見すると分かれているように見えますが、 実際には services.py の中でDBアクセスやAPIレスポンスの加工、 ビジネスルールが混在しているケースがほとんどです。

After:境界を意識したシンプルな分割

まず目指したいのは、次のような最低限の分離です。

src/
├─ domain/
│  ├─ entities.py
│  └─ services.py
├─ usecases/
│  └─ order_usecase.py
├─ interfaces/
│  ├─ repository.py
│  └─ notifier.py
├─ infrastructure/
│  ├─ db_repository.py
│  └─ email_notifier.py
└─ entrypoints/
   └─ api.py

ポイントは、domain / usecases が I/O を一切知らないことです。 DBや外部APIの詳細は、すべて infrastructure 側に閉じ込めます。

ディレクトリごとの役割を整理する

  • domain
    エンティティや純粋なビジネスルール。フレームワーク非依存。
  • usecases
    アプリケーションの振る舞いを定義。domainをどう使うかを書く。
  • interfaces
    RepositoryやNotifierなどの「契約(抽象)」を定義。
  • infrastructure
    DB、外部API、メール送信などの具体実装。
  • entrypoints
    FastAPI / CLI / バッチなど、外部からの入口。

この構成なら、Web APIをやめてCLIに切り替えても、 触るのは entrypoints だけで済みます。 ロジック層はそのまま再利用できます。

「最初から完璧」を目指さなくていい

いきなりこの構成をすべて作る必要はありません。

  • まずは「DBアクセスを別ファイルに出す」
  • 次に「ロジックをフレームワークから切り離す」
  • 必要になったらインターフェースを切る

境界設計は、一度で完成させる設計ではなく、育てていく設計です。

次の章では、この構造を支える重要な要素、 Protocol / ABC を使った「境界の作り方」を解説します。




抽象化の要:Protocol / ABCで境界を作る

ディレクトリを分けただけでは、まだ境界設計は完成していません。 本当に重要なのは、「内側が外側を知らない状態」をコードで保証することです。

そのために登場するのが、抽象化(インターフェース)です。 Pythonでは主に ABC(抽象基底クラス)と Protocol の2つが使われます。

なぜ抽象がないと境界が壊れるのか

たとえばユースケースの中で、次のようなコードを書いてしまうとどうなるでしょうか。

def create_user():
    conn = PostgresConnection()
    conn.save(...)

この時点で、ユースケースは特定のDB実装に強く依存しています。 DBを差し替えたい、テストではメモリ実装を使いたい、 そう思った瞬間に修正が必要になります。

境界設計では、ユースケースは 「保存できる何か」があることだけを知っていれば十分です。

ABCで作る明示的な契約

abc.ABC を使うと、 「このメソッドを必ず実装してください」という契約を明示できます。

from abc import ABC, abstractmethod

class UserRepository(ABC):
    @abstractmethod
    def save(self, user) -> None:
        pass

この形はJavaやC#に慣れている人には分かりやすく、 設計意図を強く伝えたい場面に向いています。

Protocolで作るPythonらしい境界

一方で typing.Protocol は、 Pythonのダックタイピングを活かした抽象化です。

from typing import Protocol

class UserRepository(Protocol):
    def save(self, user) -> None:
        ...

この場合、継承は不要で、 save() メソッドを持っていれば Repositoryとして扱えます。

静的型チェック(mypyなど)と組み合わせることで、 柔らかいのに壊れにくい境界を作れるのが強みです。

ABCとProtocol、どちらを使うべき?

  • 設計を強く固定したい → ABC
  • 柔軟性・Pythonらしさを重視 → Protocol
  • テストダブルを簡単に差し替えたい → Protocol

正解は1つではありません。 大切なのは、ユースケースが具体実装をnewしないことです。

次の章では、この抽象をどうやって実装に結び付けるのか、 依存性注入(DI)という考え方を解説していきます。




依存性注入(DI)で境界を“動かす”

Protocol や ABC で境界を定義できても、 それを実際に使えなければ意味がありません。 そこで必要になるのが 依存性注入(Dependency Injection / DI) です。

DIという言葉だけ聞くと難しそうですが、 やっていることはとてもシンプルです。

「必要なものは自分で作らず、外から渡してもらう」

newしないだけで設計は一段よくなる

境界が壊れているコードでは、 ユースケースの中で具体クラスを直接生成してしまいがちです。

class UserService:
    def create(self, user):
        repo = SqlUserRepository()
        repo.save(user)

この書き方だと、UserServiceは SqlUserRepository 以外を受け付けません。 テストや差し替えが一気に難しくなります。

コンストラクタDIの基本形

これをDIで書き直すと、次のようになります。

class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo

    def create(self, user):
        self.repo.save(user)

UserServiceは 「保存できる何か」があることしか知りません。 具体的なDB実装は、外側の世界の責任になります。

エントリーポイントで依存を組み立てる

依存関係を組み立てるのは、 APIやCLIなどのエントリーポイントです。

repo = SqlUserRepository()
service = UserService(repo)

この構造のおかげで、 テストでは簡単にスタブやフェイクに差し替えられます。

手動DIで十分なケースは多い

小〜中規模のプロジェクトでは、 この手動DIだけで十分なことがほとんどです。

  • 依存関係が追いやすい
  • 魔法が少なくデバッグしやすい
  • 学習コストが低い

それでも辛くなったらDIコンテナ

クラスが増え、 依存関係の組み立てが煩雑になってきたら、 DIコンテナの導入を検討してもOKです。

ここで大事なのは、 「DIコンテナを使うこと」ではなく 「DIできる設計になっていること」です。

既存コードを段階的に良くしていく流れについては、 次の一冊がとても参考になります。

リファクタリング(第2版)
Amazonでチェックする| ✅ 楽天でチェックする

次の章では、境界設計がもたらす最大の恩恵のひとつ、 「テストが楽になる理由」を具体的に見ていきます。




境界設計 × テスト:なぜテストが一気に楽になるのか

境界設計の効果をいちばん実感しやすいのが、テストです。 「設計を変えただけで、こんなに書きやすくなるの?」と驚く人も多いと思います。

その理由はシンプルで、 テストしたいロジックが、I/Oから完全に切り離されるからです。

境界がないとテストが重くなる

境界が曖昧なコードでは、 1つのテストを書くために次のような準備が必要になります。

  • テスト用DBの起動
  • 接続設定やマイグレーション
  • 外部APIのモック設定

本来確認したいのは 「価格計算が正しいか」「状態遷移が正しいか」なのに、 周辺準備ばかりに時間を取られてしまいます。

境界があるとテスト対象が一気に小さくなる

境界設計をすると、テストの主役は ユースケースやドメインロジックになります。

def test_create_user():
    repo = InMemoryUserRepository()
    service = UserService(repo)

    service.create(user)

    assert repo.saved_user == user

DBもAPIも不要で、 処理の流れと結果だけを素直に確認できます。

モックが最小限で済む理由

境界がはっきりしていると、 「どこを差し替えればいいか」が明確になります。

  • Repositoryはフェイク実装に差し替え
  • Notifierはダミー実装に差し替え
  • フレームワークはテスト対象外

無理にモックライブラリを多用しなくても、 素直なクラスでテストダブルを作れるのは大きなメリットです。

「テストしづらさ」は設計の警告灯

テストが書きづらいとき、 それはテスト技術の問題ではなく、 設計が苦しがっているサインであることが多いです。

境界設計は、 テストのためだけのテクニックではありません。
テストしやすい=理解しやすく、変更しやすいコードだということです。

次の章では、もう一段踏み込んで、 境界設計とドメイン思考の関係について解説していきます。




境界設計とドメイン思考の相性

境界設計を進めていくと、 あるところでこんな違和感を覚えることがあります。

「構造はきれいになったけど、ロジックが薄い気がする」 「結局、if文が増えているだけでは?」

これは失敗ではなく、次のステップへ進むサインです。 そこで重要になるのが、ドメイン思考です。

Entityを「データ入れ物」で終わらせない

境界設計を始めたばかりの頃は、 Entityがただのデータクラスになりがちです。

class Order:
    def __init__(self, price, status):
        self.price = price
        self.status = status

これでも構造的には分離できていますが、 ビジネスルールが外に漏れ出しやすくなります。

ドメイン思考では、 「ルールはデータの近くに置く」ことを重視します。

class Order:
    def confirm(self):
        if self.status != "draft":
            raise ValueError("確定できない状態です")
        self.status = "confirmed"

こうすることで、 不正な状態遷移を自然に防げるようになります。

ユースケースは「業務の流れ」を語る場所

ユースケースの役割は、 細かい計算を詰め込むことではありません。

業務として何が起きるのかを、 上から読んで分かる形で表現することです。

  • 注文を受け付ける
  • 在庫を確認する
  • 価格を確定する
  • 通知を送る

このレベルで処理が読めると、 コード自体が仕様書の代わりになります。

CRUD地獄から抜け出すヒント

ドメイン思考を取り入れると、 「Create / Update / Delete」中心の設計から、 「意味のある操作」中心の設計に変わります。

境界設計は構造を守り、 ドメイン思考は中身を豊かにする。
この2つは、とても相性がいい組み合わせです。

ドメインの考え方をやさしく理解するなら、 次の一冊がとても参考になります。

ドメイン駆動設計入門
Amazonでチェックする| ✅ 楽天でチェックする




まとめ

ここまで、Pythonにおける境界設計について、 考え方から実装、テスト、ドメイン思考まで順番に見てきました。

境界設計という言葉は少し大げさに聞こえるかもしれませんが、 本質はとてもシンプルです。

「I/Oとビジネスロジックを分ける」
まずは、これだけで十分です。

この記事で伝えたかったポイント

  • 変更に弱いコードの多くは、依存関係の向きが壊れている
  • 境界設計はPythonでも現実的に使える
  • 完璧なClean Architectureを再現する必要はない
  • Protocol / ABC + DI で「守れる境界」が作れる
  • テストしやすさは、設計の健全さを映す鏡

私の実務での実感

私自身、最初から境界設計を意識していたわけではありません。 「テストが書きづらい」「修正が怖い」という違和感から、 少しずつI/Oとロジックを分けるようになりました。

すると、設計を変えただけなのに、 コードの見通しがよくなり、 「どこを直せばいいか」で悩む時間が一気に減りました。

境界設計は、 コードを“賢くする魔法”ではありません。 でも、コードを長く健康に保つための習慣にはなります。

次にやるならこの一歩

もし今のコードが少しでも触りづらいと感じているなら、 次のどれか1つだけ試してみてください。

  • DBアクセスをロジックから切り離す
  • newしているクラスを外から渡す
  • Protocolで「できること」を定義する

小さな一歩でも、 境界は確実に育っていきます。

境界設計はゴールではなく、 よりよい設計へ進むための土台です。 ぜひ、自分のプロジェクトに合った形で取り入れてみてください 🙂


あわせて読みたい

境界設計の考え方を実務でさらに活かすために、 関連するテーマの記事もあわせて読んでみてください。
今回の内容とつながりが強く、理解が一段深まります。


参考文献


よくある質問(Q&A)

Q
境界設計は小規模なPythonプロジェクトでも必要ですか?
A

必ずしも最初からフルセットで導入する必要はありません。 ただし、小規模でもI/Oとロジックを分けるだけで、 後から機能追加や修正がかなり楽になります。

特に「将来もう少し大きくなりそう」「あとから人が増えそう」な場合は、 最低限の境界を早めに作っておく価値は高いです。

Q
Djangoの models.py は Entity と考えていいのでしょうか?
A

厳密にはそのままでは Entity とは言えません。 DjangoのモデルはORMやDBの都合を強く含んでいるため、 Clean Architectureでいう純粋なEntityとは役割が異なります。

実務では、

  • models.py は「永続化用のデータ構造」
  • domain側に「業務ルールを持つクラス」を別で置く

という分け方をすると、境界がかなり分かりやすくなります。

Q
Protocol と ABC、結局どちらを使うのが正解ですか?
A

正解はありませんが、PythonではProtocolの方が相性がいいケースが多いです。

  • テストダブルを簡単に作りたい → Protocol
  • 設計を厳密に固定したい → ABC
  • 柔軟性と読みやすさを重視 → Protocol

大切なのは選択そのものより、 ユースケースが具体クラスに依存していないことです。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク