Rubyで演算子の再定義を駆使するようなコードを書く場合、テスト時にはオブジェクトを.freeze
させたほうがいいな、と思いました。
演算子の再定義について
「C++、Ruby、Python、Swiftにあって、Java、JavaScript、PHP1にない」と、あるなしクイズみたいに有無が分かれる機能が「演算子の再定義」です。
Rubyの場合、「(複合代入を含めた)代入」と「? :
や&.
や||
のようにショートサーキットになる演算子」「スコープの::
」を除いて、ほとんどの演算子を再定義できます。
演算子をグループ分けしてみる
定義可能な演算子をグループ分けしてみます。
# 算術演算系(+@と-@は単項のプラス・マイナスです)
+ - * / % ** +@ -@
# 論理演算系
| ^ & ~
# 比較系
< <= == >= > <=> === =~
# シフト系
<< >>
# 添字系
[] []=
# 特殊用途
! !~ != `
よほど狙って特殊なものを作る2のでないかぎり、何かしらのクラスを作る上で、「算術演算系」「論理演算系」「比較系」の演算で両辺を変えてしまうのはやめたほうがいいです。「a + b
」でa
やb
が変化してしまうとなると、おそらく大混乱となるでしょうし、比較演算で値が変わってしまうのは(通常の構文中では)もはや悪夢です。
一方で、[]=
は破壊的操作を前提とした演算子ですし、Array#<<
に見られるように、シフト演算子も「破壊的な用途に使う」文化のある演算子です。
なお、「特殊用途」に挙げた否定形の演算子や、バッククオート演算子3はかなり癖がありますので、基本的には上書きしないほうがいいでしょう。
演算子のSpecを書く際に
演算子を実装するときに、「うっかり元の内容を破壊する形で実装してしまう」というトラブルもやらかしがちかもしれません。ということで、Specを書く際に、本来破壊的操作するはずでない演算子については、テストするレシーバや引数に渡すオブジェクトを事前に.freeze
するのが楽です。
.freeze
したオブジェクトに対して(故意・過失を問わず)破壊的操作をかけようとすればRuntimeError
となって、そして(.raise_error
で例外発生自体をチェックする場合は別として)例外が飛べばRSpecもFAILになってくれるので、わざわざ「元のオブジェクトが変化していないか」のチェックを書く必要もありません。