関数を設計する際、引数としてどのようなデータを受け取るべきか、迷うことはないでしょうか?例えば、ユーザーの情報が詰まった辞書やオブジェクトを丸ごと渡すべきか、それとも関数が必要とする特定のデータ(名前や年齢など)だけを渡すべきか。
結論から言えば、多くの場合において、関数が必要とする最小限の、そして最もシンプルなデータ型(int, strなど)を引数として受け取る方が、はるかに優れた設計になります。
このアプローチは、コンポーネント間の依存関係を減らす「疎結合(Loose Coupling)」という考え方に基づいています。今回は、なぜこの設計が推奨されるのか、その具体的なメリットをコードと共に見ていきましょう。
問題点:データ構造に依存した「密結合」な関数
あるユーザーの生年から年齢を計算する関数を考えてみます。ユーザーデータが辞書として与えられると想定し、次のような関数を作成しました。
import datetime
# 悪い例:ユーザー情報の辞書全体を受け取ってしまっている
def calculate_age_from_dict(user_data: dict) -> int:
"""ユーザー辞書から生年を取得し、年齢を計算する。"""
# この関数は'birth_year'というキーの存在を「知っている」必要がある
birth_year = user_data["birth_year"]
current_year = datetime.date.today().year
return current_year - birth_year
# 実行例
user_a = {"id": 1, "name": "Taro", "birth_year": 1995}
age = calculate_age_from_dict(user_a)
print(f"年齢: {age}")
この calculate_age_from_dict
関数は正しく動作しますが、いくつかの重大な問題を抱えています。
- 暗黙の依存: この関数は、引数
user_data
が辞書であり、かつ"birth_year"
というキーを持つことを暗黙的に期待しています。もし将来、キーの名前が"year_of_birth"
に変更されたり、データ構造がオブジェクトに変更されたりした場合、この関数は即座にエラーとなります。これは、関数とデータ構造が強く結びついた「密結合」の状態です。 - 低い再利用性: この関数は、
{"birth_year": ...}
という構造の辞書以外には使えません。例えば、単に1995
という数値だけを使って年齢を計算したい、といった単純な用途には全く使えないのです。 - 煩雑なテスト: この関数をテストするためには、毎回
{"birth_year": 1995}
のような辞書を作成する必要があります。年齢計算というロジックそのものをテストしたいだけなのに、余計なデータ構造の準備が必要です。
解決策:必要なデータだけを受け取る「疎結合」な関数
これらの問題を解決するには、関数が本当に必要としているデータ、つまり birth_year
という数値だけを引数として受け取るように修正します。
import datetime
# 良い例:計算に必要な「生年」というint型だけを受け取る
def calculate_age(birth_year: int) -> int:
"""生年から現在の年齢を計算する。"""
current_year = datetime.date.today().year
return current_year - birth_year
この calculate_age
関数は、先ほどの問題点をすべて解決しています。
- 明確なインターフェース: 関数シグネチャ
def calculate_age(birth_year: int)
を見るだけで、この関数が整数値のbirth_year
を必要とすることが一目瞭然です。データ構造に関する暗黙の知識は一切不要です。 - 高い再利用性: この関数は特定のデータ構造に依存しないため、あらゆる場面で再利用できます。呼び出し側が、自身の持つデータ構造から
birth_year
を取り出して渡す責任を負います。Python# 辞書から呼び出す場合 user_a = {"id": 1, "name": "Taro", "birth_year": 1995} age_a = calculate_age(user_a["birth_year"]) print(f"辞書からの計算結果: {age_a}") # 直接数値を渡す場合 age_b = calculate_age(2005) print(f"直接指定した計算結果: {age_b}")
- 容易なテスト: テストは非常にシンプルになります。必要なのは数値だけです。Python
def test_calculate_age(): assert calculate_age(1995) == 30 # 2025年時点
クラスとの連携
この疎結合な関数は、クラス設計においても非常にうまく機能します。
class User:
def __init__(self, name: str, birth_year: int):
self.name = name
self.birth_year = birth_year
@property
def age(self) -> int:
# 疎結合なユーティリティ関数を呼び出す
return calculate_age(self.birth_year)
# Userオブジェクトから年齢を取得
user_obj = User(name="Jiro", birth_year=2000)
print(f"Userオブジェクトの年齢: {user_obj.age}")
User
クラスが自身の birth_year
を calculate_age
関数に渡しています。これにより、ドメインの知識(User
が年齢を持つこと)と、純粋な計算ロジック(生年から年齢を計算すること)を綺麗に分離できています。
まとめ
関数を設計する際の原則は、**「関数が必要としない情報に依存させるな」**ということです。
- 引数は、可能な限りプリミティブな型(
int
,str
,bool
など)にする。 - 辞書やオブジェクトを渡すのではなく、呼び出し側で必要なデータを取り出して渡す。
このアプローチにより、個々の関数は独立した再利用可能な部品となり、テストも容易になります。結果として、システム全体が変更に強く、保守しやすいクリーンなアーキテクチャへと繋がっていくのです。