45395 - シコウサクゴ -

60個の共有モジュール(myModules)の設計思想:DRYとProtocolパターン

2026-04-03
AI駆動開発
AI駆動開発
Claude Code
Python
モジュール設計
DRY
Last updated:2026-04-05
10 Minutes
1963 Words

3つのエンジン(データ収集・国内データ・海外データ)を開発していると、同じコードが至るところに現れます。DB接続、Slack通知、ログ設定、日付処理——これらを各エンジンに個別実装していたら、修正が必要になった時に3箇所を直すことになります。

本記事では、60個の共有モジュールをmyModules/ディレクトリに集約した設計思想と、AI駆動開発でこの構造を活かすためのポイントを記録します。


問題:3エンジンにまたがるコード重複

本番システムは3つのエンジンで構成されています。

1
project/
2
_dataProcessingEngineA/ # データ収集エンジン
3
_dataProcessingEngineB/ # 国内データエンジン
4
_dataProcessingEngineC/ # 海外データエンジン

各エンジンは独立したプロダクトですが、以下のような共通処理が必要です。

  • PostgreSQLへの接続と切断
  • Slackへの通知送信
  • ログの初期化と出力
  • DataFrameの前処理(NaN除去、型変換)
  • プロジェクトルートの取得
  • 日付のタイムゾーン変換

初期は各エンジンにコピペしていました。Slack通知のフォーマットを変更するだけで3ファイルを編集し、1つ直し忘れてバグになるという状況が何度もありました。


解決策:myModules/ ディレクトリ

60個の共有モジュールを1つのディレクトリに集約しました。

1
myModules/
2
__init__.py # Re-exportパターン
3
mgmtDB.py # DB接続管理(PostgreSQL)
4
mgmtDF.py # DataFrame操作ユーティリティ
5
callSlack.py # Slack通知ラッパー
6
commonLogger.py # 統一ログ設定
7
dataCommon.py # 共通データユーティリティ
8
dateUtils.py # 日付・タイムゾーン処理
9
configLoader.py # 設定ファイルの読み込み
10
retryHelper.py # リトライロジック
11
... (合計60モジュール)

主要モジュールの役割

モジュール責務使用頻度
mgmtDB.pyPostgreSQL接続管理、コネクションプール全エンジン毎回
mgmtDF.pyDataFrameの加工・検証ユーティリティデータ処理時
callSlack.pySlack通知の送信(チャンネル指定、フォーマット)通知時
commonLogger.pyログレベル設定、フォーマット統一全エンジン毎回
dataCommon.pyget_project_root()等の汎用関数パス解決時

設計原則1:DRY(Don’t Repeat Yourself)

DRYの本質は「同じ知識を2箇所に書かない」ことです。

DB接続の例

_dataProcessingEngineA/db.py
1
# ❌ 各エンジンに個別実装(3箇所に同じコード)
2
import psycopg2
3
import os
4
5
def get_connection():
6
return psycopg2.connect(
7
host=os.getenv("DB_HOST"),
8
port=os.getenv("DB_PORT"),
9
dbname=os.getenv("DB_NAME"),
10
user=os.getenv("DB_USER"),
11
password=os.getenv("DB_PASSWORD"),
12
)
13
14
# _dataProcessingEngineB/db.py ← ほぼ同じコード
1 collapsed line
15
# _dataProcessingEngineC/db.py ← ほぼ同じコード
1
# ✅ myModules/mgmtDB.py に一元化
2
import psycopg2
3
import os
4
from contextlib import contextmanager
5
6
@contextmanager
7
def get_db_connection():
8
conn = psycopg2.connect(
9
host=os.getenv("DB_HOST"),
10
port=os.getenv("DB_PORT"),
11
dbname=os.getenv("DB_NAME"),
12
user=os.getenv("DB_USER"),
13
password=os.getenv("DB_PASSWORD"),
14
)
15
try:
9 collapsed lines
16
yield conn
17
finally:
18
conn.close()
19
20
# 各エンジンからの利用
21
from myModules.mgmtDB import get_db_connection
22
23
with get_db_connection() as conn:
24
df = pd.read_sql(query, conn)

DB接続パラメータの変更が必要になった場合、修正箇所はmgmtDB.pyの1箇所だけです。


設計原則2:Protocolパターン

PythonのProtocolを使うと、クラス継承なしでインターフェース契約を定義できます。

なぜProtocolか

従来のJavaスタイルの抽象クラス継承は、Python的ではない場合が多いです。特に60モジュール規模になると、継承階層が深くなるリスクがあります。

1
# ❌ 継承ベースのインターフェース
2
from abc import ABC, abstractmethod
3
4
class BaseDataFetcher(ABC):
5
@abstractmethod
6
def fetch(self, source: str, start_date: str) -> pd.DataFrame:
7
...
8
9
class SourceAFetcher(BaseDataFetcher):
10
def fetch(self, source: str, start_date: str) -> pd.DataFrame:
11
# ソースA固有の実装
12
...
13
14
class SourceBFetcher(BaseDataFetcher):
15
def fetch(self, source: str, start_date: str) -> pd.DataFrame:
2 collapsed lines
16
# ソースB固有の実装
17
...
1
# ✅ Protocolパターン(継承なし)
2
from typing import Protocol
3
import pandas as pd
4
5
class DataFetcher(Protocol):
6
def fetch(self, source: str, start_date: str) -> pd.DataFrame: ...
7
8
# Any class implementing fetch() satisfies DataFetcher
9
# No inheritance needed
10
11
class SourceAFetcher:
12
def fetch(self, source: str, start_date: str) -> pd.DataFrame:
13
# ソースA固有の実装
14
...
15
10 collapsed lines
16
class SourceBFetcher:
17
def fetch(self, source: str, start_date: str) -> pd.DataFrame:
18
# ソースB固有の実装
19
...
20
21
def run_regression_test(fetcher: DataFetcher, source: str) -> float:
22
"""DataFetcher Protocolを満たす任意のオブジェクトを受け取る"""
23
df = fetcher.fetch(source, "2024-01-01")
24
# ... 回帰テスト処理 ...
25
return score

Protocolの利点:

  • クラスが互いを知らない: SourceAFetcherDataFetcherの存在を知らなくていい
  • テストが簡単: テスト用のモッククラスも継承不要で作れる
  • 既存コードを壊さない: 後からProtocolを追加しても、既存クラスの修正は不要

設計原則3:Re-exportパターン

__init__.pyでモジュールの公開APIを定義します。

myModules/__init__.py
1
from myModules.mgmtDB import get_db_connection
2
from myModules.callSlack import send_slack_message
3
from myModules.commonLogger import setupLogger
4
from myModules.dataCommon import get_project_root
5
from myModules.mgmtDF import validate_dataframe
1
# 利用側:短いインポートで使える
2
from myModules import get_db_connection, send_slack_message, setupLogger

Re-exportパターンのメリット:

  • 内部構造の隠蔽: 利用者はmgmtDB.pyというファイル名を知らなくていい
  • リファクタリング耐性: 内部のモジュール分割を変えても、__init__.pyを更新すれば利用側は変更不要
  • AIへの指示が簡潔: 「from myModules import ...で使え」と言えば済む

AIが踏むアンチパターン

問題:AIは既存モジュールを知らない

Claude Codeに「プロジェクトルートを取得して」と依頼すると、高確率で独自の関数を再実装します。

1
# ❌ AIが自作してしまう例
2
def get_project_root():
3
current_path = Path(__file__).resolve()
4
for parent in current_path.parents:
5
if (parent / '.env').exists():
6
return parent
7
raise FileNotFoundError("Project root not found")
1
# ✅ 既存の共有モジュールを使う
2
from myModules.dataCommon import get_project_root

この問題は繰り返し発生しました。AIは「その機能が必要だ」とは理解しますが、「既に実装されている」ことを知りません。

CLAUDE.mdでの対策

1
## 共通モジュール利用ルール
2
3
以下の機能は myModules/ に実装済み。自作禁止:
4
- DB接続: myModules.mgmtDB.get_db_connection
5
- Slack通知: myModules.callSlack.send_slack_message
6
- ログ設定: myModules.commonLogger.setupLogger
7
- プロジェクトルート取得: myModules.dataCommon.get_project_root
8
- DataFrame検証: myModules.mgmtDF.validate_dataframe
9
10
新しいユーティリティ関数を作る前に、myModules/ に既存の関数がないか確認すること。

この一文をCLAUDE.mdに追加してから、AIが既存モジュールを無視して自作するケースは大幅に減りました。


命名規則:60モジュールを管理する鍵

60モジュールになると、命名規則がなければカオスになります。

採用した命名パターン

1
mgmt + 対象 : mgmtDB.py, mgmtDF.py(管理系)
2
call + サービス : callSlack.py, callAPI.py(外部呼び出し系)
3
common + 領域 : commonLogger.py, commonTypes.py(共通定義系)
4
{domain}Data + Common : dataCommon.py(ドメイン共通)
5
{動詞} + Helper : retryHelper.py, dateHelper.py(補助系)

命名規則の効果

1
# 名前だけで役割がわかる
2
from myModules.mgmtDB import ... # DBの「管理」だとわかる
3
from myModules.callSlack import ... # Slackを「呼び出す」とわかる
4
from myModules.commonLogger import ... # 「共通」のログだとわかる

AIに「Slackに通知を送りたい」と言われた時、callSlack.pyという名前は人間にもAIにも直感的です。


60モジュールの分類

1
分類 | 件数 | 例
2
----------------|------|---
3
データ管理 | 12 | mgmtDB, mgmtDF, cacheManager
4
外部サービス連携 | 10 | callSlack, callSourceA, callSourceB
5
ログ・監視 | 8 | commonLogger, metricsCollector
6
日付・時間 | 6 | dateUtils, sessionTimer
7
設定・環境 | 5 | configLoader, envValidator
8
データ変換 | 8 | transformCalc, filterGenerator
9
検証・品質管理 | 6 | qualityChecker, coverageAnalyzer
10
ユーティリティ | 5 | retryHelper, pathUtils

学んだこと

1. AIは既存モジュールを知らないと同じ関数を再実装する

CLAUDE.mdに「共通モジュールを使え」と明示的に書くことで、AIの無駄な再実装を防止できます。単に「コードベースを読め」では不十分で、具体的なモジュール名と関数名を列挙する必要があります。

2. Protocolパターンは継承なしでインターフェースを定義できる

60モジュール規模では継承階層の深さが問題になります。Protocolを使えば「このメソッドを持っていれば互換」という契約を、クラス間の結合なしに定義できます。テストのモック作成も簡単になります。

3. 60モジュールの管理は命名規則が鍵

mgmtcallcommonといったプレフィックスで役割を示す命名規則を採用しました。AIも人間も、モジュール名だけで用途を推測できます。命名規則なしに60モジュールを管理するのは現実的ではありません。


まとめ

60個の共有モジュール設計で重要なのは以下の3点です。

  1. DRYの徹底: 3エンジンの共通処理をmyModules/に一元化。DB接続やSlack通知の修正が1箇所で済む
  2. AIへの明示的な指示: CLAUDE.mdに「自作禁止、既存モジュールを使え」と書かないと、AIは同じ関数を再実装する
  3. 命名規則とRe-exportパターン: mgmt/call/commonのプレフィックスで60モジュールを整理し、__init__.pyのRe-exportで利用側のインポートを簡潔に保つ
Article title:60個の共有モジュール(myModules)の設計思想:DRYとProtocolパターン
Article author:45395
Release time:2026-04-03

記事へのご質問・ご感想をお聞かせください

フィードバックを送る