スポンサーリンク

PythonのI/Oが散らかる原因と直し方|実例で学ぶ「境界」の分離設計

クラス設計・OOP入門
  1. はじめに
  2. 1. 論点・問題提起:なぜI/Oが散らかったコードは問題なのか
    1. 変更が起きたときのダメージが大きい
    2. テストしたいだけなのに準備が大変
    3. 再利用できず、コードが増殖する
  3. 2. 背景:Python特有の事情と設計思想
    1. 関心の分離(Separation of Concerns)という考え方
    2. 依存関係の逆転という発想
  4. 3. 解決の考え方:関心の分離と依存関係の逆転
    1. 「I/Oは端っこ、ロジックは中心」
    2. 依存関係を「具体」から「抽象」へ
    3. 設計の全体像をつかむために
  5. 4. 実例①:I/Oが散らかったPythonコード
    1. 何が問題なのか
    2. テストを書こうとすると一気に辛くなる
  6. 5. 実例②:責任を分けて書き直す(第一歩)
    1. 完璧を目指さなくていい
  7. 6. 実例③:Repositoryと抽象でI/Oを完全に外へ
    1. Repositoryの役割
    2. ビジネスロジックは抽象にだけ依存する
    3. 具体的なI/O実装は外側に置く
    4. 依存性注入(DI)で組み立てる
  8. 7. テストがどう変わるか:I/O分離の最大の恩恵
    1. テストが「安心材料」になる
    2. TDDとの相性も抜群
  9. 8. よくある誤解と注意点
    1. 分離=フォルダを増やすことではない
    2. 小規模なCRUDにやりすぎない
    3. 完璧な設計を最初から目指さない
  10. まとめ:I/O分離は「未来の自分」を助ける設計
    1. 分離設計の本当のメリット
    2. まずは小さく始めよう
    3. あわせて読みたい
    4. 参考文献
  11. よくある質問(Q&A)
    1. 関連投稿:

はじめに

Pythonでコードを書いていて、こんな経験はありませんか?

  • ちょっと仕様を変えただけなのに、思った以上に修正箇所が広がってしまった
  • ロジックを確認したいだけなのに、DBや外部APIの準備が必要でテストが面倒
  • 「とりあえず動く」コードが、いつの間にか触るのが怖い存在になっている

その原因の多くは、I/O(データベース、API、ファイル操作、UI表示など)とビジネスロジックが1か所に混ざってしまっていることにあります。 Pythonは書きやすく、柔軟な言語です。その反面、気づかないうちに「全部入り」の関数やクラスが育ちやすい、という落とし穴もあります。

本記事では、Pythonでよく見かける「I/Oが散らかったコード」を題材にしながら、 なぜそれが問題になるのか、そしてどうやって直せばいいのかを、実例を交えて丁寧に解説していきます。

難しい理論を最初から振り回すことはしません。 まずは「どこが辛いのか」を言語化し、次に「少しずつ良くする方法」を順番に見ていきます。 読み終わる頃には、既存のコードを壊さずに改善する視点が手に入っているはずです。

「設計は苦手」「クリーンアーキテクチャは難しそう」と感じている方も大丈夫です。 この内容は、初心者の方だけでなく、なんとなく書けるようになってきた中級者以上の方にこそ、じわっと効いてきます。

それではまず、なぜI/Oが散らかったコードがここまで厄介なのか、その正体から見ていきましょう。




1. 論点・問題提起:なぜI/Oが散らかったコードは問題なのか

まず最初に押さえておきたいのは、「I/Oが散らかったコード」とは何かという点です。 これは単にコードが長い、汚いという話ではありません。

問題になるのは、ビジネスロジック(計算・判定・ルール)の中に、 次のような処理が直接書き込まれてしまっている状態です。

  • データベースへのアクセス
  • 外部APIの呼び出し
  • ファイルの読み書き
  • 画面表示やログ出力

一見すると「全部1か所に書いてあって分かりやすい」ように見えるかもしれません。 しかし、この構造は少し規模が大きくなると、確実に牙をむいてきます。

変更が起きたときのダメージが大きい

例えば、データベースの種類が変わったり、APIのレスポンス形式が変わったりした場合を考えてみてください。 本来であればI/Oの修正だけで済むはずなのに、 ロジックと密結合していると、無関係な判定処理まで巻き込んで修正することになります。

「このif文、DBの変更に関係あるんだっけ?」と悩みながら触るコードは、 それだけで開発者のストレスになりますよね。

テストしたいだけなのに準備が大変

ロジックがI/Oに直接依存していると、テストも一気に面倒になります。

  • テストのたびにDBを起動する
  • 外部APIのモックを大量に用意する
  • 環境依存のエラーに振り回される

本当は「計算結果が正しいか」を確認したいだけなのに、 テストの主役がI/Oの準備になってしまうのは、よくある失敗です。

再利用できず、コードが増殖する

さらに厄介なのが再利用性の低下です。 Web API用に書いた処理を、CLIツールやバッチ処理で使いたくなったとき、 I/Oがべったり埋め込まれていると、そのまま流用できません。

結果として、 「ほぼ同じロジックだけど、I/Oが違うコード」が量産されていきます。 バグ修正も二重・三重に必要になり、保守コストはどんどん膨らみます。

こうした問題の根っこにあるのが、 「違う理由で変更されるものが、同じ場所に詰め込まれている」という設計上のミスです。

次の章では、なぜPythonでは特にこの問題が起きやすいのか、 そしてそれを防ぐための設計思想について整理していきます。




2. 背景:Python特有の事情と設計思想

I/Oが散らかったコードは、決して「書き方が下手だから」生まれるわけではありません。 むしろPythonという言語の書きやすさ・柔軟さが、その原因になることも多いです。

Pythonでは、少ないコード量で「動くもの」を素早く作れます。 そのため、最初は次のような流れになりがちです。

  • 1つの関数に処理を書いてみる
  • ついでにDBアクセスも足す
  • ログ出力やprintもその場で追加
  • 気づいたら全部入り関数になっている

小規模なうちは問題が見えません。 ですが、機能追加や仕様変更が重なるにつれて、 「どこを直せばいいのか分からないコード」に成長してしまいます。

関心の分離(Separation of Concerns)という考え方

この問題を解決するための基本となる考え方が、 関心の分離(SoC)です。

難しく聞こえますが、考え方はとてもシンプルです。

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

例えば、

  • 計算ルールが変わる理由
  • DBの種類が変わる理由
  • API仕様が変わる理由

これらは、まったく別の事情で起こります。 にもかかわらず、同じ関数・同じクラスに詰め込まれていると、 変更のたびにコード全体が揺さぶられてしまいます。

依存関係の逆転という発想

もう一つ重要なのが、依存関係の逆転という考え方です。

多くのコードでは、 「ビジネスロジック → DBやAPI」といった形で、 ロジックがI/Oの詳細に直接依存しています。

分離設計では、この向きをひっくり返します。

ビジネスロジックは、 「どう保存されるか」「どこから取得されるか」を知りません。 ただ「保存できる何か」「取得できる何か」に対して処理を依頼するだけです。

その結果、 I/Oの変更がロジックに波及しない構造が作れるようになります。

次の章では、この考え方をもう一段具体化し、 「どうやってコードに落とし込むのか」を整理していきます。 ここからが、実践編です。




3. 解決の考え方:関心の分離と依存関係の逆転

ここまでで、「I/Oが散らかったコードがなぜ辛いのか」は、かなりはっきりしてきたと思います。 では実際に、どう考えればコードは良くなるのかを整理していきましょう。

ポイントは難しいテクニックではありません。 意識すべきなのは、コードの中に“境界”を作ることです。

「I/Oは端っこ、ロジックは中心」

分離設計を一言で表すなら、 「I/Oはアプリケーションの端に追い出す」という考え方になります。

アプリケーションの中心に置きたいのは、 次のような純粋なビジネスロジックです。

  • どんな条件で処理が分岐するのか
  • どんな計算を行うのか
  • どんなルールを守るべきか

これらは、本来 DBの種類やAPIの仕様とは無関係なはずです。 にもかかわらず、I/Oが混ざることで中心が見えなくなってしまいます。

依存関係を「具体」から「抽象」へ

ここで登場するのが、依存関係の逆転という考え方です。

悪い依存の例は、こんな形です。

  • ビジネスロジックが PostgreSQL を直接呼ぶ
  • ビジネスロジックが requests でAPIを叩く

これを、次のように置き換えます。

  • 「保存できるもの」に依存する
  • 「取得できるもの」に依存する

つまり、具体的な実装ではなく、役割(抽象)に依存するということです。 そうすると、あとから DBを差し替えても、APIを変更しても、 中心のロジックは一切触らずに済むようになります。

設計の全体像をつかむために

ここまでの話は、実は場当たり的なテクニックではありません。 体系化された設計思想として整理されています。

その代表例が、クリーンアーキテクチャです。 「エンティティ」「ユースケース」「I/O」という層に分け、 依存の向きを常に内側へ向けるというルールを持っています。

もし「なぜそこまで分けるのか」「全体像をちゃんと理解したい」と感じたら、 次の一冊がとても参考になります。

Clean Architecture
Amazonでチェックする |✅ 楽天でチェックする

とはいえ、最初から理論を完璧に理解する必要はありません。 次の章では、実際にどんなコードが「ダメ」なのかを見ながら、 分離設計がなぜ効くのかを体感していきます。




4. 実例①:I/Oが散らかったPythonコード

ここからは、実際のコードを見ながら話を進めていきます。 まずはよくある「I/Oが全部入り」な例からです。

次のコードは、一見すると特におかしなことはしていません。 データを取得して、判定して、結果を保存する。 実務でも本当によく見かける形です。


def register_user(user_id: int):
    # DBからユーザー情報を取得
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute(
        "SELECT age FROM users WHERE id = %s",
        (user_id,)
    )
    row = cursor.fetchone()

    if row is None:
        print("ユーザーが見つかりません")
        return

    age = row[0]

    # ビジネスロジック
    if age < 20:
        print("未成年のため登録できません")
        return

    # DBへ保存
    cursor.execute(
        "UPDATE users SET registered = true WHERE id = %s",
        (user_id,)
    )
    conn.commit()

    print("登録が完了しました")
  

このコードの中には、次の3種類の関心が混ざっています。

  • データベースへのアクセス(取得・更新)
  • 年齢による判定というビジネスルール
  • printによる結果表示

何が問題なのか

問題は、「これらが同じ理由で変更されるわけではない」点にあります。

  • DBがPostgreSQLから別のDBに変わる
  • 年齢制限のルールが変わる
  • CLIではなくWeb APIで結果を返したくなる

どれか1つが変わるだけで、 この関数全体を触らなければならなくなります。 しかも、どこまでが影響範囲なのかを毎回考える必要があります。

テストを書こうとすると一気に辛くなる

この関数をテストしようとすると、どうなるでしょうか。

  • テスト用のDBを用意する
  • 事前にユーザーデータを仕込む
  • printの内容をどう検証するか悩む

本当は、 「20歳以上なら登録されるか」 というロジックだけを確認したいはずです。 それなのに、テストの準備が主役になってしまいます。

こうしたコードは、書いた直後は楽ですが、 時間が経つほど「触りたくないコード」に変わっていきます。

次の章では、このコードを少しずつ分解しながら、 どうやってI/Oとロジックを切り離していくのかを見ていきましょう。 いきなり完璧を目指す必要はありません。




5. 実例②:責任を分けて書き直す(第一歩)

いきなり「クリーンアーキテクチャっぽく書き直そう」とすると、 それだけで身構えてしまいますよね。 ここではまず、今あるコードを壊さずに改善する第一歩を見ていきます。

ポイントはシンプルです。 先ほどの関数に含まれていた処理を、次の3つに分けて考えます。

  • Input: データを取得する処理(DB・APIなど)
  • Algorithm: 判定・計算などのビジネスロジック
  • Output: 結果を外へ伝える処理(保存・表示など)

まずは、ロジック部分だけを関数として切り出してみましょう。


def can_register(age: int) -> bool:
    if age < 20:
        return False
    return True
  

たったこれだけですが、 年齢による判定ルールがI/Oから完全に独立しました。 この関数は、DBもAPIもprintも知りません。

次に、元の関数からロジックを呼び出す形にします。


def register_user(user_id: int):
    conn = get_db_connection()
    cursor = conn.cursor()

    cursor.execute(
        "SELECT age FROM users WHERE id = %s",
        (user_id,)
    )
    row = cursor.fetchone()

    if row is None:
        print("ユーザーが見つかりません")
        return

    age = row[0]

    if not can_register(age):
        print("未成年のため登録できません")
        return

    cursor.execute(
        "UPDATE users SET registered = true WHERE id = %s",
        (user_id,)
    )
    conn.commit()

    print("登録が完了しました")
  

見た目はあまり変わっていませんが、 設計的には大きな一歩です。

  • ビジネスルールが1か所に集まった
  • ロジック単体でテストできるようになった
  • 仕様変更時の影響範囲が狭くなった

完璧を目指さなくていい

この段階では、まだI/Oは分離しきれていません。 それで問題ありません。

大切なのは、 「巨大な関数を、小さな責任に分ける」 という癖をつけることです。 この積み重ねが、後で紹介するRepositoryやDIにつながっていきます。

既存コードを安全に、少しずつ改善していく考え方については、 次の一冊がとても参考になります。

Refactoring
Amazonでチェックする |✅ 楽天でチェックする

次の章では、ここからさらに一歩進めて、 I/Oそのものを外へ追い出す設計を見ていきます。 Repositoryパターンと抽象の出番です。




6. 実例③:Repositoryと抽象でI/Oを完全に外へ

前の章で、ビジネスロジックを関数として切り出しました。 ここからはさらに一歩進めて、 I/Oそのものをロジックの外に追い出す設計を見ていきます。

そのために使うのが、Repositoryパターン抽象(インターフェース)です。

Repositoryの役割

Repositoryは、「どこにどう保存されているか」を隠し、 ビジネスロジックにとって必要な操作だけを提供します。

まずは、Repositoryの“契約”を定義します。 Pythonでは typing.Protocol を使うと、自然に書けます。


from typing import Protocol, Optional

class UserRepository(Protocol):
    def find_age(self, user_id: int) -> Optional[int]:
        ...

    def mark_registered(self, user_id: int) -> None:
        ...
  

ここで重要なのは、 SQLもDBの種類も一切出てこないことです。 ビジネスロジックが知っているのは「年齢を取得できる」「登録済みにできる」という事実だけです。

ビジネスロジックは抽象にだけ依存する

次に、ユースケース側のコードを書き換えます。


def register_user(user_id: int, repo: UserRepository) -> None:
    age = repo.find_age(user_id)

    if age is None:
        print("ユーザーが見つかりません")
        return

    if not can_register(age):
        print("未成年のため登録できません")
        return

    repo.mark_registered(user_id)
    print("登録が完了しました")
  

この関数は、もうDBに直接触れていません。 どんな保存先であっても、 UserRepositoryという契約を満たしていれば動くようになりました。

具体的なI/O実装は外側に置く

実際のDBアクセスは、別クラスとして用意します。


class SqlUserRepository:
    def __init__(self, conn):
        self._conn = conn

    def find_age(self, user_id: int) -> Optional[int]:
        cursor = self._conn.cursor()
        cursor.execute(
            "SELECT age FROM users WHERE id = %s",
            (user_id,)
        )
        row = cursor.fetchone()
        return None if row is None else row[0]

    def mark_registered(self, user_id: int) -> None:
        cursor = self._conn.cursor()
        cursor.execute(
            "UPDATE users SET registered = true WHERE id = %s",
            (user_id,)
        )
        self._conn.commit()
  

これで、 DBの詳細はすべてアプリケーションの外側に隔離されました。

依存性注入(DI)で組み立てる

最後に、エントリーポイントで実体を組み立てます。


def main():
    conn = get_db_connection()
    repo = SqlUserRepository(conn)
    register_user(user_id=1, repo=repo)
  

こうして、 ロジック → 抽象 ← I/O という依存関係が完成します。

Repositoryやサービス層を含めた設計パターンを体系的に学びたい場合は、 次の一冊がとても参考になります。

Architecture Patterns with Python
Amazonでチェックする |✅ 楽天でチェックする

次の章では、 この設計によってテストがどれだけ楽になるのかを、 実例で体感していきましょう。




7. テストがどう変わるか:I/O分離の最大の恩恵

I/O分離の効果が一番はっきり表れるのが、テストです。 ここまで設計を進めてきたコードは、 もはや「DBがなければ動かない」存在ではありません。

まず、Repositoryをテスト用の実装に差し替えてみましょう。


class InMemoryUserRepository:
    def __init__(self, users: dict[int, int]):
        self._users = users
        self._registered = set()

    def find_age(self, user_id: int):
        return self._users.get(user_id)

    def mark_registered(self, user_id: int) -> None:
        self._registered.add(user_id)
  

これは、DBを一切使わないRepositoryです。 辞書だけで振る舞いを再現しています。

これを使うと、テストは次のように書けます。


def test_register_user_success():
    repo = InMemoryUserRepository({1: 25})
    register_user(user_id=1, repo=repo)

    assert 1 in repo._registered
  

テストに、DB接続もAPIモックもありません。 ロジックだけを、数ミリ秒で確認できます。

テストが「安心材料」になる

I/Oが混ざったコードでは、テストはどうしても重くなります。 その結果、 「テストを書くのが面倒」→「書かない」 という流れに陥りがちです。

分離設計をすると、テストは一気に身近になります。

  • ロジック変更が怖くなくなる
  • リファクタのスピードが上がる
  • バグの混入にすぐ気づける

テストは「作業」ではなく、 コードを守るための保険に変わります。

TDDとの相性も抜群

こうした構造は、 テスト駆動開発(TDD)とも非常に相性が良いです。

先に「どう振る舞うべきか」をテストで書き、 あとからRepositoryの実装を差し替える。 そんな進め方が自然にできるようになります。

PythonでのTDDや、テスト設計の考え方をじっくり学びたい場合は、 次の一冊が定番です。

Test-Driven Development with Python
Amazonでチェックする |✅ 楽天でチェックする

次の章では、 分離設計でよくある誤解や注意点を整理していきます。 「やりすぎ」を防ぐための話です。




8. よくある誤解と注意点

I/O分離やクリーンな設計の話をすると、 「それ、難しそう」「大規模開発向けでしょ?」と身構えられがちです。 ですが、実際には誤解されやすいポイントがいくつかあります。

分離=フォルダを増やすことではない

よくある勘違いが、 「ディレクトリ構成を分ければ設計も良くなる」というものです。

もちろん整理は大切ですが、 本質は依存関係の向きにあります。 フォルダが分かれていても、ロジック層がDB実装を直接importしていれば、 設計としては何も改善されていません。

小規模なCRUDにやりすぎない

単純なCRUDアプリケーションに、 最初からRepositoryやUseCaseを全部そろえると、 かえってコードが読みにくくなることもあります。

大切なのは、 「辛くなったところから分離する」という姿勢です。 まずはロジックを関数として切り出す。 それだけでも十分に価値があります。

完璧な設計を最初から目指さない

設計の本や記事を読むと、 「こう書くべき」という正解があるように感じてしまいます。 でも、現実のコードはもっと泥臭いものです。

分離設計は、 未来の変更に耐えるための準備です。 今すぐ必要ない抽象を無理に入れる必要はありません。

日々のコードを少しずつ良くしていく、 その感覚を養うのに役立つ一冊がこちらです。

Clean Code
Amazonでチェックする |✅ 楽天でチェックする




まとめ:I/O分離は「未来の自分」を助ける設計

ここまで、PythonでI/Oが散らかったコードがなぜ辛くなるのか、 そしてそれをどう直していけばいいのかを、実例ベースで見てきました。

振り返ってみると、やっていること自体はとてもシンプルです。

  • ロジックとI/Oを同じ場所に書かない
  • ビジネスルールを中心に据える
  • 具体的な実装ではなく「役割」に依存する

これだけで、コードは驚くほど変更に強くなります。

分離設計の本当のメリット

I/O分離というと、 「テストが楽になる」「設計がきれいになる」 といった技術的なメリットがよく語られます。

でも、私が一番大きいと感じているのは、 コードを触るときの心理的な負担が減ることです。

  • 変更するのが怖くなくなる
  • 影響範囲を考えやすくなる
  • 「ここはロジック」「ここはI/O」と頭の中で整理できる

これは、チーム開発でも個人開発でも、 確実に効いてきます。

まずは小さく始めよう

最初からRepositoryやDIを完璧に導入する必要はありません。

まずは、 「このif文、I/Oと一緒に書く必要ある?」 と立ち止まってみること。 そこから関数を1つ切り出すだけでも、立派な一歩です。

I/O分離は、 一度やって終わりのテクニックではありません。 コードと長く付き合うための習慣です。

この記事が、 「このコード、ちょっと分けてみようかな」 と思うきっかけになれば、とても嬉しいです 🙂


あわせて読みたい

I/O分離や設計の考え方は、一度理解すると他のテーマにも一気につながっていきます。 ここでは、今回の記事と特に相性の良い関連記事をピックアップしました。

気になるものから読んでみてください。 設計の理解が、ぐっと立体的になるはずです。


参考文献

設計の考え方には流派や表現の違いがありますが、 共通して語られているのは「I/Oとビジネスロジックを分けることで、変更とテストに強くなる」という点です。 気になるものから読み進めてみてください。


よくある質問(Q&A)

Q
I/O分離は小規模なスクリプトでも必要ですか?
A

結論から言うと、必ずしも最初から必要ではありません。 数十行で完結するワンショットのスクリプトであれば、無理に分離しなくても問題ないことが多いです。

ただし、「あとで少し機能を足すかもしれない」「テストを書きたくなりそう」 と感じた時点で、ロジックだけでも切り出しておくと後が楽になります。 I/O分離は将来の変化が見えた瞬間から効き始める設計だと考えてください。

Q
Protocolと抽象基底クラス(ABC)はどちらを使うべきですか?
A

どちらが正解、というものはありませんが、使い分けの目安はあります。

  • Protocol: 「この形を満たしていればOK」という柔らかい契約を作りたいとき。 既存クラスを後付けで対応させたい場合にも向いています。
  • ABC: 継承関係をはっきりさせたい場合や、 実行時にもインターフェース違反を検知したい場合に向いています。

今回のようなRepositoryの例では、 Pythonらしい書き心地を重視してProtocolを選ぶケースが増えています。

Q
FastAPIやDjangoでも同じ考え方は使えますか?
A

はい、むしろフレームワークを使うほど効果が高くなります

FastAPIのエンドポイント関数や、 Djangoのビューにビジネスロジックを直接書いてしまうと、 フレームワーク依存が一気に強くなります。

ロジックをユースケースとして切り出し、 フレームワーク側は「入力を受け取って呼び出すだけ」にしておくと、 テストもしやすく、将来的な構成変更にも耐えやすくなります。

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

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

スポンサーリンク