関数を設計する上で、その関数が「何をするか」を明確に定義することは、コードの品質を左右する重要な要素です。多くの関数は、値を計算して返すだけのものと、システムの状態(DBのデータ、ファイル、オブジェクトの属性など)を変更するものの2種類に大別できます。
問題は、これら2つの役割が一つの関数に混在している場合に発生します。特に、何かを**問い合わせる(Query)ように見える関数が、裏でシステムの状態を命令(Command)**して変更してしまう「副作用(Side Effect)」を持つと、コードは非常に予測しにくく、危険なものになります。
副作用を持つ関数の具体例
あるユーザープロフィールの情報(名前、メールアドレス)を更新する処理を考えます。更新前に、ユーザー名が禁止ワードリストに含まれていないかを検証するis_profile_valid
という関数を作成しました。
# 悪い例:問い合わせを行う関数が、副作用としてデータの更新・保存を行っている
# ユーザーを表すシンプルなクラス
class User:
def __init__(self, name: str, email: str, status: str = "PENDING"):
self.name = name
self.email = email
self.status = status
def save(self):
# 本来はデータベースに保存する処理
print(f"--- {self.name}の状態を保存しました (Status: {self.status}) ---")
# 禁止されているユーザー名のリスト
BANNED_USERNAMES = {"admin", "root", "superuser"}
def is_profile_valid(user: User) -> bool:
"""ユーザープロフィールの有効性を検証する"""
if user.name in BANNED_USERNAMES:
return False
# ★問題点:検証関数の中で、状態の変更と保存(副作用)を行っている
user.status = "APPROVED"
user.save()
return True
def update_user_profile(user: User, new_name: str, new_email: str):
"""ユーザーのプロフィールを更新する"""
user.name = new_name
user.email = new_email
# この関数を呼び出すだけで、userの状態が変更・保存されてしまう
if not is_profile_valid(user):
print("エラー: プロフィールが有効ではありません。")
return None
# 本来はこちらで保存処理をしたいが、is_profile_valid内で実行済み
# user.save() #
return user
# 実行例
user_a = User(name="guest", email="guest@example.com")
update_user_profile(user_a, "taro", "taro@example.com")
なぜこれが問題なのか?
is_profile_valid
という名前は、プロフィールが有効かどうかを問い合わせるだけの関数に見えます。しかし、その内部では user
オブジェクトの status
を変更し、save()
メソッドを呼び出して永続化するという、重大な命令を実行しています。
もし、別の開発者が単に「このユーザーは有効かな?」と状態を確認したいだけ(更新はしたくない)のつもりで is_profile_valid
を呼び出した場合、意図せずユーザーのステータスが APPROVED
に更新され、データベースに保存されてしまいます。
このように、関数の名前に反した副作用は、発見が困難なバグの温床となります。
解決策:問い合わせと命令を分離する (CQS原則)
この問題を解決するのが、**コマンド・クエリ分離(Command-Query Separation, CQS)**という設計原則です。
- 問い合わせ (Query): システムの状態を返すだけで、副作用を一切持たない。
- 命令 (Command): システムの状態を変更するだけで、値を返さない(または自分自身を返す程度)。
この原則に従って、先ほどのコードを修正します。
1. 副作用のない「問い合わせ」関数を定義する
まず、ユーザー名が有効かどうかを検証するだけの、純粋な「問い合わせ」関数を作成します。この関数は引数として渡された文字列を評価し、真偽値を返すことだけに責任を持ちます。
# 良い例①:副作用のない、純粋な問い合わせ関数
def is_username_allowed(username: str) -> bool:
"""ユーザー名が許可されているかを検証し、真偽値を返す。"""
return username not in BANNED_USERNAMES
この関数は、引数で渡された username
以外、外部の状態に一切影響を与えません。そのため、いつ、どこで、何度呼び出しても安全です。
2. 状態の変更を「命令」関数に集約する
次に、ユーザープロフィールの更新という「命令」の責任をすべて負う関数を定義します。この関数の中で、先ほど作成した「問い合わせ」関数を呼び出します。
# 良い例②:状態の変更という責務を集約した命令関数
def update_user_profile(user: User, new_name: str, new_email: str):
"""ユーザーのプロフィールを更新し、状態を保存する。"""
# 1. 副作用のない関数で問い合わせる
if not is_username_allowed(new_name):
print(f"エラー: ユーザー名 '{new_name}' は許可されていません。")
return None
# 2. この関数内で、状態の変更と保存という命令をすべて行う
user.name = new_name
user.email = new_email
user.status = "APPROVED"
user.save() # 状態の変更がすべて終わってから、最後に保存する
return user
修正後の update_user_profile
は、ロジックの流れが非常に明確です。
- ユーザー名が許可されているか問い合わせる。
- 許可されていなければ、処理を中断する。
- 許可されていれば、プロフィールの更新とステータスの承認という命令を実行し、最後に保存する。
すべての状態変更が一つの関数にまとまっているため、コードの振る舞いが予測しやすくなりました。
まとめ
関数の副作用は、それ自体が悪なのではありません。データベースの更新やファイルの書き込みなど、副作用は多くのアプリケーションで必要不可欠です。問題なのは、副作用が関数の名前に反して、隠れた場所で実行されることです。
- 問い合わせ (Query) の関数:
is_...
,has_...
,get_...
といった名前の関数は、副作用を持たないように設計する。 - 命令 (Command) の関数:
update_...
,create_...
,save_...
といった名前の関数に、状態を変更する処理を集約する。
この「問い合わせ」と「命令」を意識的に分離することで、コードは各段にテストしやすく、再利用性が高く、そして何より安心して読むことができるようになります。