命名と期待される動作
命名と期待される動き
~副作用を伴わない読み取り処理の設計~
はじめに
プログラムにおいて命名することよりその命名された関数だったり変数に対する期待が生まれる重要な要素であることは確かですよね。特に、「get」や「fetch」といった読み取りを意味する名称は呼び出し時に副作用が発生せず、単に値を解すことを暗黙の了解としていますよね。私は、命名されたものがどのような動きをするのか経験則で感覚のように扱っていました。そのせいで、設計上のNGを作りそうになったため言語化のために記事にします。
詳細内容
この記事では、データが存在しない場合にどのようなアプローチを取るべきか、すなわち「初期値を返すだけ」と「初回呼び出し時にデフォルト値をセット(挿入)して返す」方法の違いと、それぞれの設計上の特徴について考察しています。
命名から読み取る期待
命名規則と直感的理解
-
読み取り専用を示す命名
関数名に「get」や「fetch」が含まれているとき、一般的な利用者はその関数が外部状態を変更しない、すなわち副作用がないことを期待するでしょう。
例えば、getUserProfile()
やfetchConfig()
といった関数名はただ単にデータを返すだけと期待するでしょう。言い方を偏らせれば、DBのテーブルなどの状態は変化しないことが期待されています。 -
状態を変更を示唆する命名
一方、createOrUpdateUserProfile()
やinitialConfig
といった関数名は、何らかの状態変更、特にデータの挿入や更新が行われることが期待されますね。
この違いは、当たり前ですがコードを読むだけで関数の役割や挙動が予測できるため、メンテナンス性の向上やバグの早期発見につながります。<- ものすごい当たり前なことを言っています。
副作用のない読み取り処理の重要性
一貫性と予測可能性
-
純粋な関数のメリット:
状態変更を伴わない関数は、同じ入力に対して常に同じ出力を返す(冪等性)ため、プログラム自体の動作が予測しやすくなる。
これにより、テストやデバックが容易になり、意図しない副作用による不具合(悪魔)を呼ばなくて済むことになりますね。 -
キャッシュやリトライ戦略との相性
ちょっとコアなのかもしれませんが、外部状態を変更しないため、キャッシュ戦略やリトライ処理を実装する際にも副作用を気にする必要がなく、システムの堅牢性が向上しちゃうんですね!まあ嬉しい。
初回値返却と初回挿入のアプローチの比較
ここでは私が実際にNGしかけた具体的な比較をしたいと思います。関数の呼び出し時に対象データが存在しなかった場合の2つのアプローチを比較します!
-
初期値返却(副作用なし)
-
動作:
関数が呼ばれた際、データが存在しなければあらかじめ定義された初期値をそのまま返す。 -
メリット:
- 副作用がない: データベースや内部状態を変更しないため、何度呼んでも同じ結果が保証される。
- シンプルな実装: 実装が単純で、状態管理や競合状態を考慮する必要がない。
-
デメリット:
- 状態保持が必要な場合の対応:
実際のデータが後で更新されるケースでは、初期値が一時的なものであることを利用者側で明示する工夫が必要になる可能性がある。
- 状態保持が必要な場合の対応:
-
動作:
-
初回挿入(副作用あり)
-
動作:
関数呼び出し時、対象データが存在しない場合、初期値をデータベースや内部ストレージに挿入し、その値を返す。
以降は、挿入済みのデータを返す形となる。 -
メリット:
- 状態の一貫性:一度挿入すれば、その後は統一された状態として扱われ、更新や参照が容易になる場合がある。
-
デメリット:
- 副作用が発生する:
関数が単なる読み取り操作であっても、状態が変更されるため、呼び出しタイミングによって結果が変わる可能性がある。 - 実装の複雑化:
複数の呼び出しが同時に発生した場合や、状態の初期化が重複するリスク - 予期しない動作:
命名より期待する動作と実際にする動作の乖離により利用者に混乱も招く可能性がある。
- 副作用が発生する:
-
動作:
設計ガイドラインと推奨事項
-
命名は挙動の指針とする:
読み取り専用の関数などは、get
やfetch
などの命名を用い、状態変化を行わない実装とする。
状態変化を含む処理には、明示的な命名(例:init
,create
,update
)を使い、利用者にその性質を伝える。ここの処理に入れちゃえば楽だと思い、入れた処理が命名と離れないると悪魔が誕生される。気をつけよう。 -
純粋性を保つ:
特に複数回呼び出される可能性がある関数では、副作用がない設計(純粋関数)を心がける。
これにより、予期せぬ動作やデバックの難しさを防止できる。 -
状態管理が必要な場合は専用の処理を用意する:
初期化処理やデータ挿入は、読み取りとは別の関数として切り出して、明確に責務を分離することで、コードの意図を保つ!
結論
命名とは、利用者にその関数などがどのような動作をするのかを直感的に伝える重要な要素であるんだよね!
読み取りを意図するような関数名は副作用を持たずに設計し、利用者に混乱をもたらさないようにしよう。
最終的には、役割と命名から利用者が期待する動作を明確にし、設計時にその原則を厳守することが、堅牢かつ保守性の高いシステム開発につながるといえるんだろう!
著者について
takumi0706です。エンジニアになりたい人でして、現在はバックエンド開発(フロントもちょっと)を頑張ってます。技術的な挑戦を続ける中で学んだことをアウトプットすることをなるたけ努力してます。
- ポートフォリオサイト: takumi0706.simple.ink
- GitHub: takumi0706
- X(旧Twitter): @1ye_q
これからも技術的な知見を深め、共有していくことを目指していますので、ぜひフィードバックをお寄せください。
takumi0706
Discussion