はじめに
あるプロダクトで、複数のAPIから共通のSELECTのためのrepository.selectを呼び出す構成を取っていました。
イメージとしては次のような形です。
A処理 ⇒ repository.select
B処理 ⇒ repository.select
当初はシンプルで問題なく動作していましたが、プロダクトが成長するにつれて課題が出てきました。
発生した課題
A処理の要件が増え、repository.selectに新しいパラメータを追加する必要が生じました。
その結果、
- B処理側は大きな変更は不要なのに、
- 共通repository.selectに依存しているため、B処理側も修正を余儀なくされる
という状況になりました。
さらに、使用している言語が動的型付け言語だったため、
- 引数が合わなくても実行できてしまう
- テストをすり抜けたデグレードが発生するリスクが高まる
という問題が顕在化してきました。
改善の方針
そこで、次のように呼び出し構造を整理しました。
A処理 ⇒ repository.select
B処理 ⇒ 中間メソッド ⇒ repository.select
実装イメージ
# 共通の repository.select
# 期間などのフィルタは任意指定。None は「その条件を適用しない」意味で扱う。
repository.select(user_id=123, start_date=date(2025,1,1), end_date=date(2025,1,31))
# 中間メソッド
def select_user_summary(user_id: int):
"""
B処理は期間指定を行わないユースケース。
明示的に None を渡して repository.select に委譲する。
"""
return repository.select(
user_id=user_id,
start_date=None, # フィルタ無し
end_date=None, # フィルタ無し
)
# B処理側は専用の中間メソッドを呼ぶだけ
rows = select_user_summary(123)
- B処理用の中間メソッドを新設
- B処理は、自分に必要なパラメータだけを渡す
- 中間メソッドは、それ以外のパラメータに対しては
None
を明示的に設定して repository.selectへ委譲
この仕組みが成り立つ前提として、repository.select
の新規パラメータは任意(オプション)であり、None
= 条件未適用 という契約を守る必要があります。必須化するような破壊的変更は吸収できません。
改善後のメリット
- 意図しない引数の混入を防止できる
- B処理側のコード修正が最小限で済む
- デグレのリスクを低減でき、安心してA処理側の進化に対応可能
- テストコードでA処理・B処理がそれぞれ独立に動作確認できる
特に検索系では「デフォルト条件の解釈違い」がバグ源になりやすいため、None=条件未適用
を契約として固定するのは効果的。
補足: 他のアプローチとの比較
今回のケースでは「中間メソッドを設ける」方法を採用しました。
他のアプローチと比較すると次のようになります。
1. 型ヒント・型チェックの導入
動的型付け言語では、mypy
や pydantic
のようなツールを用いた型チェックやスキーマバリデーションが考えられます。
ただし、今回の本質である 「A処理の拡張がB処理に波及する」構造的依存 は解決しません。
一方で、中間層の契約(例:None
の意味)を型で表現することで退行を早期に検知できるため、中間層+型導入の併用は有効。
2. 共通メソッドをAPIごとに分割する
repository.selectを A処理用・B処理用に完全分割する方法も考えられます。
ただし完全に別メソッドにすると、共通処理の重複が増え保守コストが高まります。
今回の 中間メソッド(ファサード/Adapter) は、呼び出し契約を分離しつつ、共通のクエリ組み立ては repository に集約できる点でバランスが取れています。
3. 中間層を設ける(今回採用)
-
メリット:
- A処理の進化がB処理に波及しない
- 共通処理は repository.selectに集約でき、重複も最小化できる
- 実装コストが低い
-
デメリット:
-
None
を「条件未指定」として扱うチーム規約の共有が必要
-
今回の判断
- 型導入では依存関係の問題は解決しない
- 完全分割では重複コードが増える
このため、中間メソッドを設ける方法が最もバランスが良いと判断しました。
結果として、A処理の拡張時にB処理への修正は不要となり、デグレードのリスクを大きく下げることができたと考えています。
まとめ
共通化は便利ですが、成長するプロダクトの中では「引数肥大化」がリスクになると思います。
もともとはA処理、B処理で同じ責任を果たしていたところ、A処理が進化することによって、もともとのrepository.selectが単一責任の原則から外れつつあったと考えています。
今回の取り組みでは、中間層を設けることで依存関係を整理し、デグレードを防ぐ仕組みを導入できました。
- 共通化のメリットとデメリットを天秤にかける
- 必要に応じて「責務を切り分ける」
この2点を意識することで、将来の変更に強い設計を作れると考えています。