多くのPythonプロジェクトにおいて、utils.py
やhelpers.py
、common.py
といった名前のファイルを見かけます。これらは、どこに置くべきかすぐには判断がつかない便利な関数をとりあえず入れておく「便利屋」あるいは「ガラクタ置き場」のようなモジュールとして、つい作られがちです。
しかし、このような汎用的な名前のモジュールに様々な機能を詰め込む習慣は、プロジェクトが成長するにつれて、コードの可読性や保守性を著しく低下させる大きな技術的負債となります。今回は、なぜこの「便利屋モジュール」が問題なのか、そしてコードを健全に保つためのモジュール設計の原則について解説します。
問題点:無関係な機能が混在した「低凝集」なモジュール
ソフトウェア設計には**「凝集度(Cohesion)」**という重要な指標があります。これは、一つのモジュール内に含まれる機能が、どれだけ強く関連し合っているかを示す度合いです。理想的なモジュールは「高凝集」であり、一つの明確な責任を持ちます。
utils.py
のようなファイルは、この原則を破り「低凝集」になりがちです。例えば、あるWebアプリケーションのutils.py
に、次のような全く無関係な関数が混在しているとしましょう。
悪い例:utils.py
に様々な種類の関数が混在
# utils.py
# 1. データベースアクセスに関するロジック
from .models import User
def get_premium_users():
return User.objects.filter(is_premium=True, is_active=True)
# 2. URLのクエリパラメータを操作するロジック
from urllib.parse import urlencode
def build_url_with_query(base_url, params):
return f"{base_url}?{urlencode(params)}"
# 3. 日付操作に関する汎用的なロジック
from datetime import date, timedelta
def get_next_monday(d: date) -> date:
days_ahead = 7 - d.weekday()
return d + timedelta(days=days_ahead)
このモジュールは、ユーザーデータ、URL、日付計算という、全く異なる3つの「関心事」を抱えています。このような低凝集なモジュールは、以下の問題を引き起こします。
- 発見性の低下:
get_premium_users
という関数を探すとき、users/models.py
を探すべきか、utils.py
を探すべきか分かりません。コードのどこに何があるのかが不明瞭になります。 - 再利用性の低下: URL操作の関数だけを別のプロジェクトで使いたくても、
utils.py
にはUser
モデルへの依存があるため、モジュール全体を単純に再利用することができません。 - 循環インポートのリスク:
utils.py
が多くのモデルやモジュールをインポートし、逆に多くのモジュールがutils.py
をインポートするようになると、循環インポートエラーの温床となります。
解決策:責任とドメインに基づいてモジュールを分割する
解決策は、各関数を、それが本来属するべき、より専門的なモジュールに配置することです。
1. ドメイン固有のロジックは、そのドメインの場所へ
「プレミアムユーザーを取得する」というロジックは、明らかにUser
というドメインに強く関連しています。このようなデータベースクエリは、Djangoの慣習に従い、models.py
のカスタムQuerySet
やManager
に配置するのが最適です。
users/models.py
class UserQuerySet(models.QuerySet):
def premium_users(self):
return self.filter(is_premium=True, is_active=True)
class User(models.Model):
# ...
objects = UserQuerySet.as_manager()
# 呼び出し側: User.objects.premium_users()
2. 責任が明確な新しいモジュールを作成する
「URLのクエリを組み立てる」というロジックは、HTTPリクエストに関連する処理です。これをrequest_helpers.py
やurls.py
といった、責任が明確な新しいモジュールに移動させます。
utils/request_helpers.py
from urllib.parse import urlencode
def build_url_with_query(base_url, params):
return f"{base_url}?{urlencode(params)}"
3. utils.py
は、真に汎用的な関数のために残す
「次の月曜日を計算する」という日付操作は、特定のアプリケーションのドメインに依存しない、真に汎用的なユーティリティです。このような、プロジェクト固有の知識を必要としない関数は、utils/date_helpers.py
のようなモジュールに配置するか、あるいは数が少なければ、そのような純粋なヘルパーだけを集めたutils.py
に残しても良いでしょう。重要なのは、utils.py
にドメイン固有のロジックを入れないというルールを徹底することです。
まとめ
モジュールを設計する際は、常に**「この関数は、どの仲間と一緒にいるのが最も自然か?」**と自問自答する習慣をつけましょう。
- 特定のモデルに強く関連する処理 →
models.py
のQuerySet
やメソッドへ - 特定の機能領域(例: 認証、API連携、フォーマット)に関する処理 →
auth.py
,api.py
,formatting.py
のような専門モジュールを作成 - プロジェクトのどこからでも使え、ドメイン知識に依存しない純粋な処理 →
utils.py
やhelpers.py
に置くことを検討
utils.py
を便利なゴミ箱として使うのではなく、各モジュールが高い凝集度を持つように意識してファイルを分割することで、プロジェクトは整理され、見通しが良く、長期的なメンテナンスが容易なものになります。