プログラミングにおいて、関数はコードを整理し、再利用性を高めるための基本的な単位です。しかし、ただ単に繰り返し現れるコードをまとめるだけでは、真に読みやすく、変更に強いコードにはなりません。重要なのは、その関数が**「何を」するのか**だけでなく、**「なぜ」その処理が行われるのか**という「意味」や「目的」でまとめることです。
これは「関心の分離(Separation of Concerns)」と呼ばれる設計原則に通じる考え方であり、各関数が明確な一つの「関心事」にのみ責任を持つように設計することで、コードの可読性、保守性、テスト容易性が飛躍的に向上します。
処理でまとめた関数の問題点
例えば、次のようなPythonスクリプトを考えてみましょう。このスクリプトは、商品データが記載されたCSVファイルを読み込み、特定の条件(価格が100未満)を満たす商品の名前を出力します。
import csv
# 悪い例:処理のまとまりで関数化しているため、意図が不明瞭
def process_and_print_items(filepath: str):
"""
指定されたファイルパスから商品を読み込み、
価格が特定の条件を満たす商品名を出力する。
"""
with open(filepath, mode="r", encoding="utf-8") as file:
reader = csv.reader(file)
next(reader) # ヘッダー行をスキップ
for row_data in reader:
# ここに処理の塊がある
product_name = row_data[0]
product_price = int(row_data[1])
if product_price < 100: # 特定の条件
print(product_name)
# 実行
if __name__ == "__main__":
# 仮のCSVファイルを作成
with open("sample_products.csv", "w", encoding="utf-8") as f:
f.write("Name,Price\n")
f.write("Apple,120\n")
f.write("Banana,80\n")
f.write("Orange,95\n")
f.write("Grape,250\n")
process_and_print_items("sample_products.csv")
一見すると、process_and_print_items
という関数に処理がまとまっているように見えます。しかし、この関数は以下の複数の「関心事」を抱えています。
- ファイルからCSVを読み込む
- 各行のデータを解析する(文字列から数値への変換など)
- 特定の条件で価格を判定する
- 結果を出力する
このように複数の責務を持つ関数は、次のような問題を引き起こします。
- 意図が分かりにくい: 関数名だけでは、具体的に「何のために」これらの処理が行われているのかが伝わりにくい。
- 再利用性が低い: 「CSVからデータを読み込むだけ」「価格を判定するだけ」といった個別の処理を別の場所で使いたい場合、この関数をそのまま再利用できない。
- 単体テストが困難: ファイル操作、データ解析、条件判定、出力といった異なる要素が混ざっているため、それぞれのロジックを独立してテストするのが難しい。
解決策:意味のまとまりで関数化する
これらの問題を解決するには、各関数が「意味のある一つの関心事」に集中するようにコードを分割します。
1. データの読み込みと解析 (read_products_from_csv
)
ファイルから商品データを読み込み、適切な型に変換して返すことだけを担う関数を作成します。Pythonのジェネレーターを使うことで、メモリ効率良くデータを処理できます。
# 良い例①:データの読み込みと解析に特化
def read_products_from_csv(filepath: str):
"""
CSVファイルから商品名と価格を読み込み、
(商品名: str, 価格: int)のタプルをジェネレーターとして返す。
"""
with open(filepath, mode="r", encoding="utf-8") as file:
reader = csv.reader(file)
next(reader) # ヘッダーをスキップ
for row_data in reader:
product_name = row_data[0]
product_price = int(row_data[1])
yield product_name, product_price # ジェネレーターとして返す
この関数は、「CSVから商品を読み込む」という明確な意味のまとまりを持ちます。
2. 条件の判定 (is_promotional_price
)
価格がプロモーション対象(例: 100未満)かどうかを判定する関数です。この関数は純粋な「問い合わせ」であり、副作用を持ちません。
# 良い例②:条件判定に特化
def is_promotional_price(price: int) -> bool:
"""指定された価格がプロモーション対象(100未満)かを判定する。"""
return price < 100
is_promotional_price
という名前から、この関数が価格の条件判定を行うことが自明です。
3. メインロジック (display_promotional_items
)
最後に、これらの関数を組み合わせて、最終的な目的を達成するメインのロジックを記述します。
# 良い例③:メインロジック (目的達成のための組み合わせ)
def display_promotional_items(filepath: str):
"""
CSVファイルからプロモーション対象の商品を抽出し、その名前を表示する。
"""
products = read_products_from_csv(filepath) # 読み込み
for name, price in products:
if is_promotional_price(price): # 判定
print(f"プロモーション対象: {name}") # 出力
# 実行例
if __name__ == "__main__":
# 仮のCSVファイルを作成 (再利用)
with open("sample_products.csv", "w", encoding="utf-8") as f:
f.write("Name,Price\n")
f.write("Apple,120\n")
f.write("Banana,80\n")
f.write("Orange,95\n")
f.write("Grape,250\n")
display_promotional_items("sample_products.csv")
このように分割することで、
read_products_from_csv
: ファイルI/Oとデータ解析の責務だけを持つ。is_promotional_price
: 純粋な条件判定の責務だけを持つ。display_promotional_items
: 全体の流れを制御し、各部品を組み合わせる責務を持つ。
と、各関数が明確な「意味」のまとまりを持つようになりました。
まとめ
関数を分割する際は、単にコード行数を減らすためではなく、**その関数が「どのような目的」を達成するために存在するか**という視点で考えることが重要です。
- 意味のあるまとまりで関数化する: 各関数に「関心事」を一つだけ持たせる。
- 処理の意図を明確にする: 関数名で「何を」するだけでなく「なぜ」するのかを推測させる。
- 再利用性とテスト容易性を高める: 独立した部品として機能させることで、様々な場面での利用や、単体テストが容易になる。
「この関数は何のため、誰のためにあるのか?」という問いを常に持ち、意味のまとまりを意識した関数設計を心がけましょう。