3つのエンジン(データ収集・国内データ・海外データ)を開発していると、同じコードが至るところに現れます。DB接続、Slack通知、ログ設定、日付処理——これらを各エンジンに個別実装していたら、修正が必要になった時に3箇所を直すことになります。
本記事では、60個の共有モジュールをmyModules/ディレクトリに集約した設計思想と、AI駆動開発でこの構造を活かすためのポイントを記録します。
問題:3エンジンにまたがるコード重複
本番システムは3つのエンジンで構成されています。
1project/2 _dataProcessingEngineA/ # データ収集エンジン3 _dataProcessingEngineB/ # 国内データエンジン4 _dataProcessingEngineC/ # 海外データエンジン各エンジンは独立したプロダクトですが、以下のような共通処理が必要です。
- PostgreSQLへの接続と切断
- Slackへの通知送信
- ログの初期化と出力
- DataFrameの前処理(NaN除去、型変換)
- プロジェクトルートの取得
- 日付のタイムゾーン変換
初期は各エンジンにコピペしていました。Slack通知のフォーマットを変更するだけで3ファイルを編集し、1つ直し忘れてバグになるという状況が何度もありました。
解決策:myModules/ ディレクトリ
60個の共有モジュールを1つのディレクトリに集約しました。
1myModules/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.py | PostgreSQL接続管理、コネクションプール | 全エンジン毎回 |
mgmtDF.py | DataFrameの加工・検証ユーティリティ | データ処理時 |
callSlack.py | Slack通知の送信(チャンネル指定、フォーマット) | 通知時 |
commonLogger.py | ログレベル設定、フォーマット統一 | 全エンジン毎回 |
dataCommon.py | get_project_root()等の汎用関数 | パス解決時 |
設計原則1:DRY(Don’t Repeat Yourself)
DRYの本質は「同じ知識を2箇所に書かない」ことです。
DB接続の例
1# ❌ 各エンジンに個別実装(3箇所に同じコード)2import psycopg23import os4
5def 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 に一元化2import psycopg23import os4from contextlib import contextmanager5
6@contextmanager7def 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 conn17 finally:18 conn.close()19
20# 各エンジンからの利用21from myModules.mgmtDB import get_db_connection22
23with 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# ❌ 継承ベースのインターフェース2from abc import ABC, abstractmethod3
4class BaseDataFetcher(ABC):5 @abstractmethod6 def fetch(self, source: str, start_date: str) -> pd.DataFrame:7 ...8
9class SourceAFetcher(BaseDataFetcher):10 def fetch(self, source: str, start_date: str) -> pd.DataFrame:11 # ソースA固有の実装12 ...13
14class SourceBFetcher(BaseDataFetcher):15 def fetch(self, source: str, start_date: str) -> pd.DataFrame:2 collapsed lines
16 # ソースB固有の実装17 ...1# ✅ Protocolパターン(継承なし)2from typing import Protocol3import pandas as pd4
5class DataFetcher(Protocol):6 def fetch(self, source: str, start_date: str) -> pd.DataFrame: ...7
8# Any class implementing fetch() satisfies DataFetcher9# No inheritance needed10
11class SourceAFetcher:12 def fetch(self, source: str, start_date: str) -> pd.DataFrame:13 # ソースA固有の実装14 ...15
10 collapsed lines
16class SourceBFetcher:17 def fetch(self, source: str, start_date: str) -> pd.DataFrame:18 # ソースB固有の実装19 ...20
21def run_regression_test(fetcher: DataFetcher, source: str) -> float:22 """DataFetcher Protocolを満たす任意のオブジェクトを受け取る"""23 df = fetcher.fetch(source, "2024-01-01")24 # ... 回帰テスト処理 ...25 return scoreProtocolの利点:
- クラスが互いを知らない:
SourceAFetcherはDataFetcherの存在を知らなくていい - テストが簡単: テスト用のモッククラスも継承不要で作れる
- 既存コードを壊さない: 後からProtocolを追加しても、既存クラスの修正は不要
設計原則3:Re-exportパターン
__init__.pyでモジュールの公開APIを定義します。
1from myModules.mgmtDB import get_db_connection2from myModules.callSlack import send_slack_message3from myModules.commonLogger import setupLogger4from myModules.dataCommon import get_project_root5from myModules.mgmtDF import validate_dataframe1# 利用側:短いインポートで使える2from myModules import get_db_connection, send_slack_message, setupLoggerRe-exportパターンのメリット:
- 内部構造の隠蔽: 利用者は
mgmtDB.pyというファイル名を知らなくていい - リファクタリング耐性: 内部のモジュール分割を変えても、
__init__.pyを更新すれば利用側は変更不要 - AIへの指示が簡潔: 「
from myModules import ...で使え」と言えば済む
AIが踏むアンチパターン
問題:AIは既存モジュールを知らない
Claude Codeに「プロジェクトルートを取得して」と依頼すると、高確率で独自の関数を再実装します。
1# ❌ AIが自作してしまう例2def get_project_root():3 current_path = Path(__file__).resolve()4 for parent in current_path.parents:5 if (parent / '.env').exists():6 return parent7 raise FileNotFoundError("Project root not found")1# ✅ 既存の共有モジュールを使う2from myModules.dataCommon import get_project_rootこの問題は繰り返し発生しました。AIは「その機能が必要だ」とは理解しますが、「既に実装されている」ことを知りません。
CLAUDE.mdでの対策
1## 共通モジュール利用ルール2
3以下の機能は myModules/ に実装済み。自作禁止:4- DB接続: myModules.mgmtDB.get_db_connection5- Slack通知: myModules.callSlack.send_slack_message6- ログ設定: myModules.commonLogger.setupLogger7- プロジェクトルート取得: myModules.dataCommon.get_project_root8- DataFrame検証: myModules.mgmtDF.validate_dataframe9
10新しいユーティリティ関数を作る前に、myModules/ に既存の関数がないか確認すること。この一文をCLAUDE.mdに追加してから、AIが既存モジュールを無視して自作するケースは大幅に減りました。
命名規則:60モジュールを管理する鍵
60モジュールになると、命名規則がなければカオスになります。
採用した命名パターン
1mgmt + 対象 : mgmtDB.py, mgmtDF.py(管理系)2call + サービス : callSlack.py, callAPI.py(外部呼び出し系)3common + 領域 : commonLogger.py, commonTypes.py(共通定義系)4{domain}Data + Common : dataCommon.py(ドメイン共通)5{動詞} + Helper : retryHelper.py, dateHelper.py(補助系)命名規則の効果
1# 名前だけで役割がわかる2from myModules.mgmtDB import ... # DBの「管理」だとわかる3from myModules.callSlack import ... # Slackを「呼び出す」とわかる4from myModules.commonLogger import ... # 「共通」のログだとわかるAIに「Slackに通知を送りたい」と言われた時、callSlack.pyという名前は人間にもAIにも直感的です。
60モジュールの分類
1分類 | 件数 | 例2----------------|------|---3データ管理 | 12 | mgmtDB, mgmtDF, cacheManager4外部サービス連携 | 10 | callSlack, callSourceA, callSourceB5ログ・監視 | 8 | commonLogger, metricsCollector6日付・時間 | 6 | dateUtils, sessionTimer7設定・環境 | 5 | configLoader, envValidator8データ変換 | 8 | transformCalc, filterGenerator9検証・品質管理 | 6 | qualityChecker, coverageAnalyzer10ユーティリティ | 5 | retryHelper, pathUtils学んだこと
1. AIは既存モジュールを知らないと同じ関数を再実装する
CLAUDE.mdに「共通モジュールを使え」と明示的に書くことで、AIの無駄な再実装を防止できます。単に「コードベースを読め」では不十分で、具体的なモジュール名と関数名を列挙する必要があります。
2. Protocolパターンは継承なしでインターフェースを定義できる
60モジュール規模では継承階層の深さが問題になります。Protocolを使えば「このメソッドを持っていれば互換」という契約を、クラス間の結合なしに定義できます。テストのモック作成も簡単になります。
3. 60モジュールの管理は命名規則が鍵
mgmt、call、commonといったプレフィックスで役割を示す命名規則を採用しました。AIも人間も、モジュール名だけで用途を推測できます。命名規則なしに60モジュールを管理するのは現実的ではありません。
まとめ
60個の共有モジュール設計で重要なのは以下の3点です。
- DRYの徹底: 3エンジンの共通処理を
myModules/に一元化。DB接続やSlack通知の修正が1箇所で済む - AIへの明示的な指示: CLAUDE.mdに「自作禁止、既存モジュールを使え」と書かないと、AIは同じ関数を再実装する
- 命名規則とRe-exportパターン:
mgmt/call/commonのプレフィックスで60モジュールを整理し、__init__.pyのRe-exportで利用側のインポートを簡潔に保つ