はじめに
Djangoで開発していると、コード自体は動いているものの、あとから変更しづらくなっている箇所に出会うことがあります。
最近、自分の中ではその危険信号を大きく2つに分けて見るようになりました。それが、
- 依存が深い
- 依存が広い
の2つです。かなり雑ですが、先に結論をいうと、
-
a.b.c.dのように、何段もたどっているのが「深い依存」 - 同じような参照や条件が、あちこちに散っているのが「広い依存」
です。
この記事では、会社での交通費精算を例にしながら、Djangoにおける「依存の深さ」と「依存の広さ」について整理します。
厳密な設計パターンの解説というより、コードを読んだときに「ここはあとでつらくなりそう」と気づくための観察メモに近い内容です。
深い依存とは
たとえば、会社で交通費を精算する場面を考えます。
営業部の社員が、出張の交通費を精算したいとします。
このとき、営業部の社員としては、本来は
この交通費は精算できるのか?
が分かれば十分です。
普通は、経費精算システムに入力したり、経理に確認したりすればよいはずです。
しかし、もし毎回こんなことをしないといけないとしたらどうでしょうか。
社員情報を見る
→ 所属部署を見る
→ 雇用区分を見る
→ 出張規程を見る
→ 交通費精算ルールを見る
→ 承認フローを見る
→ 承認可能か判断する
これはかなり危うい状態です。
営業部の人が、交通費を精算するために、会社内部の構造を何段も知っていないといけません。
もちろん、実際の会社では裏側でいろいろなルールが関係しているはずです。
しかし、日常業務をする人が、その内部構造を毎回たどらないと仕事ができないなら、仕組みとしてはかなり壊れやすくなります。
問題は、ルールや組織構造はあとから変わることです。
依存が深ければ深いほど、本来、自分の業務とはそんなに関係ないはずのルール変更に巻き込まれる可能性が上がります。
そして、新しいルールへの適応にはコストと時間がかかります。
これが、この記事でいう「依存が深い」状態に近いです。
Djangoでいう「依存が深い」状態
Djangoでいうと、たとえば次のようなコードです。
employee.department.company.expense_policy.transportation_rule.limit_amount
このように . が何段も続いているコードは、一見すると便利です。
オブジェクトの関連をたどれば、欲しい情報に直接アクセスできます。
しかし、呼び出し側が内部構造をかなり知っている状態でもあります。
たとえば、次のような前提を呼び出し側が知っていることになります。
- Employee は Department に所属している
- Department は Company に所属している
- Company は ExpensePolicy を持っている
- ExpensePolicy は TransportationRule を持っている
- TransportationRule は limit_amount を持っている
つまり、交通費を確認したいだけの処理が、会社・部署・規定・ルールの構造まで知ってしまっています。
これは、営業部の社員が交通費精算のために、会社の内部構造を何段もたどっている状態に似ています。
深い依存がつらくなる場面
深い依存は、途中の構造が変わったときにつらくなります。
たとえば、次のような変更があったとします。
- 部署を経由せず、社員が直接会社に所属するようになった
- 交通費ルールが会社単位ではなく部署単位になった
- expense_policy が nullable になった
- transportation_rule が複数種類に分かれた
- 一部の社員だけ別ルールになった
このとき、深い参照があちこちにあると、変更の影響範囲が読みにくくなります。
employee.department.company.expense_policy.transportation_rule.limit_amount
このコードは、途中のどれか1つが変わるだけで壊れる可能性があります。
また、実はテストデータを作るときも大変になります。
「交通費の上限を確認するだけ」のテストなのに、
- Employee
- Department
- Company
- ExpensePolicy
- TransportationRule
を全部用意しないといけない、ということが起きます。
これは、コードの見通しを悪くします。
テストが失敗したときに原因を追いにくくなるだけでなく、そもそもテストデータを用意するための前提が増えてしまうからです。
深い依存への対処
深い依存を見つけたときは、まず
呼び出し側がここまで内部構造を知る必要があるのか?
を考えます。
会社の例でいえば、営業部の社員が毎回、
社員情報
→ 所属部署
→ 雇用区分
→ 出張規程
→ 交通費精算ルール
→ 承認フロー
をたどるのではなく、経費精算システムに
この社員のこの交通費は承認できるか?
を問い合わせる形に近いです。
営業部の社員は、出張規程や承認フローの内部構造をすべて知る必要はありません。
必要なのは、最終的に「承認できるか」「上長承認が必要か」が分かることです。
Djangoでも同じで、呼び出し側が深い関連を毎回たどるのではなく、意味のあるメソッドや関数に閉じ込めることで、内部構造への依存を減らせる場合があります。
たとえば、次のように1つのメソッドに閉じ込められるかもしれません。
employee.get_transportation_limit_amount()
あるいは、経費精算の判定として切り出すなら、次のような形も考えられます。
can_approve = expense_service.can_approve_transportation(employee, amount)
重要なのは、必ずメソッド化・Service化すべきという話ではありません。
1箇所だけなら、そのままでよい場合もあります。
ただし、同じような深い参照が何度も出てくるなら、呼び出し側が内部構造を知りすぎているサインかもしれません。
広い依存とは
依存の深さとは別に、「依存が広い」状態もあります。これは深い状態とはまた別の壊れ方をします。
会社の例で考えてみます。
たとえば、
片道2,000円以上の交通費は上長承認が必要
というルールが、
片道1,500円以上の交通費は上長承認が必要
に変わったとします。
このルールが経費精算システムの1箇所にまとまっていれば、そこを変更すれば済みます。
しかし、もし同じ判断がいろいろな部署や資料に散らばっていたらどうでしょうか。
- 営業部のマニュアル
- 経理部のチェックリスト
- 総務部のFAQ
- 上長向けの承認フロー
- 社員向けの申請画面
- 月次レポートの集計条件
それぞれに「2,000円以上なら承認が必要」と書かれていると、ルール変更時に修正漏れが起きます。
1つ1つの記述は難しくありません。
しかし、同じ判断が広い範囲に散らばっているため、全体として壊れやすくなっています。
これが、この記事でいう「依存が広い」状態です。
Djangoでいう「依存が広い」状態
Djangoでいうと、次のような条件が複数箇所に散らばっている状態です。
Expense.objects.filter(
employee__department__company=request.user.company,
is_deleted=False,
)
この条件が、たとえば次のような場所に出てくるとします。
- 一覧View
- 詳細View
- 更新View
- Form
- Admin
- 集計処理
- CSV出力
- テスト
1箇所ずつ見ると、それほど悪く見えないかもしれません。
Expense.objects.filter(
employee__department__company=request.user.company,
is_deleted=False,
)
しかし、同じような条件があちこちに散らばっていると、仕様変更時に修正漏れが起きやすくなります。
たとえば、次のような変更が入った場合です。
-
is_deleted=Falseではなくis_active=Trueを見ることになった - 会社単位ではなく部署単位で絞ることになった
- 組織管理者だけは全部署を見られるようになった
- 承認済みの経費だけ表示することになった
- 一部のロールでは閲覧条件が変わった
このとき、条件が複数箇所に散らばっていると、全部を正しく修正する必要があります。
どこか1箇所でも漏れると、表示漏れや権限漏れにつながる可能性があります。
なお、この例は employee__department__company という深い参照でもあります。
このように、深い依存が複数箇所に散らばると、深さと広さの問題が同時に起きます。
広い依存への対処
広い依存を見つけたときは、
同じ判断を複数箇所で書いていないか?
を考えます。
会社の例でいえば、営業部のマニュアル、経理部のチェックリスト、総務部のFAQ、社員向け申請画面に、それぞれ同じ交通費ルールを書くのではなく、交通費精算ルールの原本を1箇所に置くようなものです。
各部署はルールを自前で持つのではなく、必要なときにその原本や経費精算システムの判定結果を参照します。
そうしておけば、2,000円以上から1,500円以上にルールが変わったときも、各部署の資料を全部探して直す必要が減ります。
Djangoでも同じで、同じ表示条件・権限条件・除外条件が複数箇所に散っているなら、QuerySetなどに名前をつけて寄せることで、変更箇所を絞れる場合があります。
たとえば、次の条件が何度も出てくるなら、
Expense.objects.filter(
employee__department__company=request.user.company,
is_deleted=False,
)
QuerySetに寄せることを考えられます。
class ExpenseQuerySet(models.QuerySet):
def visible_to(self, user):
return self.filter(
employee__department__company=user.company,
is_deleted=False,
)
呼び出し側は、次のように書けます。
Expense.objects.visible_to(request.user)
こうすると、呼び出し側は「経費がどういう条件で見えるか」を細かく知る必要がありません。
また、条件を変えるときも、まず visible_to() を見ればよくなります。
もちろん、何でもQuerySetに押し込めばよいわけではありません。
似ているけれど意味が違う条件を無理にまとめると、逆に分かりにくくなります。
大事なのは、
これは同じ判断なのか?
それとも、たまたま似ているだけの別の判断なのか?
を見分けることです。
深い依存と広い依存は壊れ方が違う
依存の深さと広さは、似ているようで壊れ方が違います。
| 種類 | 危険信号 | つらくなる場面 |
|---|---|---|
| 深い依存 |
a.b.c.d のように何段もたどる |
内部構造が変わったとき |
| 広い依存 | 同じ条件や参照が複数箇所に散らばる | 仕様変更時に修正漏れが起きるとき |
深い依存は、コードを1箇所見ただけでも気づきやすいです。
employee.department.company.expense_policy.transportation_rule.limit_amount
このようなコードを見ると、「ちょっと深いな」と感じやすいです。
一方で、広い依存は1箇所だけ見ても分かりにくいです。
Expense.objects.filter(
employee__department__company=request.user.company,
is_deleted=False,
)
これ自体は普通のコードに見えるかもしれません。
しかし、同じ条件がView、Form、Admin、集計処理、テストに散らばっていると、変更時の影響範囲が広くなります。
個人的には、依存の広さの方が見落としやすく、あとから効いてくることが多いと感じています。
特に権限まわりでは、あとから
- 全件見えていたものを、直属の部下の申請だけに絞る
- 一部のロールだけ閲覧条件を変える
- 所属部署や承認状態によって表示範囲を変える
といった変更が入りやすく、条件が散らばっていると修正漏れが起きやすくなります。
ただし、最初から全部を抽象化しない
ここまで書くと、
深い参照は全部メソッド化しよう
重複条件は全部QuerySetにまとめよう
と思うかもしれません。
しかし、それも危険です。
1回しか出てこない処理を無理に抽象化すると、かえって読みにくくなることがあります。
また、似ているだけで意味が違う処理をまとめると、あとから分岐だらけの共通処理になります。
そのため、自分は次のように考えるのがよいと思っています。
1回だけ出てくる深い参照
まずはそのままでもよい。
ただし、テストが書きづらい、nullableで不安、意味が読み取りづらいなら切り出しを検討する。
何度も出てくる深い参照
呼び出し側が内部構造を知りすぎている可能性がある。
Modelのメソッド、QuerySet、Service層などに閉じ込められないか考える。
1回だけ出てくる条件
まずはそのままでもよい。
ただし、権限やセキュリティに関わるなら早めに名前をつけた方がよい場合もある。
複数箇所に出てくる条件
広い依存になっている可能性がある。
特に、権限・所属・公開状態・ソフトデリートなどの条件は、散らばると修正漏れが怖い。
まとめ
Djangoでコードを書いているとき、自分は最近以下の2つを見るようにしています。
-
.が深く続いていないか - 同じような条件が複数箇所に散らばっていないか
前者は「依存の深さ」です。
呼び出し側が、相手の内部構造を知りすぎている状態です。
後者は「依存の広さ」です。
同じ判断が複数箇所に散らばり、仕様変更時に修正漏れが起きやすい状態です。
どちらも、今すぐバグになるとは限りません。
しかし、コードが大きくなったり、仕様変更が入ったり、権限管理が複雑になったりすると、あとから効いてきます。
最初から完璧な設計を目指す必要はありません。
ただ、
この参照は深すぎないか?
この条件は広がりすぎていないか?
という見方を持っておくだけで、Djangoアプリの見通しはかなり良くなると思います。