
値が不変なプログラムの書き方
こんにちは。エンジニアの今井です。
最近のプログラミングでは、テスト容易性や保守性などの観点から値の不変性が重視されています。
今回はそんな値の不変性を実現する手法を2つご説明します。
1. 純粋関数
純粋関数とは以下の条件を満たした関数のことです。
・入力に対して出力が一意に決まる。
・副作用が発生しない
「入力に対して出力が一意に決まる」とは、例えば以下のようなメソッドのことを言います。
このようなメソッドは「〇回繰り返すと挙動が変わる」というようなことがないため、テストが容易になります。
<span class="token keyword">int</span> <span class="token keyword">add</span><span class="token punctuation">(</span><span class="token keyword">int</span> value1<span class="token punctuation">,</span> <span class="token keyword">int</span> value2<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
<span class="token keyword">return</span> value1 <span class="token operator">+</span> value2<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span>
逆に以下のようなものは純粋関数ではありません。このような関数の場合、テストが難しく、また処理を追うのも難しいです。
<span class="token comment">// 値を変更している</span>
<span class="token keyword">void</span> <span class="token keyword">add</span><span class="token punctuation">(</span><span class="token keyword">ref</span> <span class="token keyword">int</span> value1<span class="token punctuation">,</span> <span class="token keyword">int</span> value2<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
value1 <span class="token operator">+=</span> value2<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span>
<span class="token comment">// 外部の値に結果が依存している</span>
<span class="token keyword">int</span> value2
<span class="token keyword">int</span> <span class="token keyword">add</span><span class="token punctuation">(</span><span class="token keyword">int</span> value1<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
<span class="token keyword">return</span> value1 <span class="token operator">+</span> value2<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span>
さて、副作用とはなんでしょうか?
これは値を変更しないことに加えて、外部システムと連携しないということなどを指します。
例えば、以下のコードは結果が一意ですが副作用があります。
<span class="token keyword">void</span> <span class="token keyword">add</span><span class="token punctuation">(</span><span class="token keyword">int</span> value1<span class="token punctuation">,</span> <span class="token keyword">int</span> value2<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">(</span>value1 <span class="token operator">+</span> value2<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span>
上記は標準出力なので画面を見ればデバッグできますが、ユニットテストができなくなってしまいます。
さらに、副作用がDBや外部サービスのAPIになってしまった場合、相手側の通信や状態によって、戻ってくる結果が異なることも明らかです。
こうなってくると、テスト環境の構築も難しくなります。
言い換えれば、副作用がない場合はテスト環境の構築が容易ですし、何よりも純粋なロジックの世界で処理を書くことができます。
このようにメソッドが純粋関数だとテストが簡単になり、動作も容易に予想ができるようになります。
2. 値オブジェクト
値オブジェクトとは、コンストラクタ以外で値が変化しないオブジェクトのことを言います。
初期化ですべてが決まるため、他のクラスにオブジェクトを渡してもステートをいちいち考慮しなくていいのが魅力的な手法です。
値オブジェクトの例は以下のものがあげられます(説明のため、C#の糖衣構文である自動実装プロパティは利用していません)。
<span class="token keyword def">class</span> hoge<span class="token punctuation">{</span>
<span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token keyword">int</span> foo<span class="token punctuation">;</span>
<span class="token keyword">public</span> <span class="token keyword">int</span> <span class="token function">Get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword this">this</span><span class="token punctuation">.</span>foo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token function">hoge</span><span class="token punctuation">(</span><span class="token keyword">int</span> foo<span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword this">this</span><span class="token punctuation">.</span>foo <span class="token operator">=</span> foo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span>
値オブジェクトではないものは以下になります。
以下の例の場合、hogeオブジェクトを扱う間、他の誰かがfooの値を変化していないか意識し続ける必要があります。
<span class="token comment">// Setメソッドがあることによって値が変化可能になっている</span>
<span class="token keyword def">class</span> hoge<span class="token punctuation">{</span>
<span class="token keyword">private</span> <span class="token keyword">int</span> foo<span class="token punctuation">;</span>
<span class="token keyword">public</span> <span class="token keyword">int</span> <span class="token function">Get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword this">this</span><span class="token punctuation">.</span>foo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">Set</span><span class="token punctuation">(</span><span class="token keyword">int</span> foo<span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword this">this</span><span class="token punctuation">.</span>foo <span class="token operator">=</span> foo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token function">hoge</span><span class="token punctuation">(</span><span class="token keyword">int</span> foo<span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword this">this</span><span class="token punctuation">.</span>foo <span class="token operator">=</span> foo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span>
このように値オブジェクトでクラスを書くと、オブジェクトを一度生成すれば、その後はオブジェクト内の値を意識しなくて良いため、状態管理が楽になります。
3. おわりに
値オブジェクトとは、コンストラクタ以外で値が変化しないオブジェクトのことを言います。
初期化ですべてが決まるため、他のクラスにオブジェクトを渡してもステートをいちいち考慮しなくていいのが魅力的な手法です。
値オブジェクトの例は以下のものがあげられます(説明のため、C#の糖衣構文である自動実装プロパティは利用していません)。
<span class="token keyword def">class</span> hoge<span class="token punctuation">{</span>
<span class="token keyword">private</span> <span class="token keyword">readonly</span> <span class="token keyword">int</span> foo<span class="token punctuation">;</span>
<span class="token keyword">public</span> <span class="token keyword">int</span> <span class="token function">Get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword this">this</span><span class="token punctuation">.</span>foo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token function">hoge</span><span class="token punctuation">(</span><span class="token keyword">int</span> foo<span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword this">this</span><span class="token punctuation">.</span>foo <span class="token operator">=</span> foo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span>
値オブジェクトではないものは以下になります。
以下の例の場合、hogeオブジェクトを扱う間、他の誰かがfooの値を変化していないか意識し続ける必要があります。
<span class="token comment">// Setメソッドがあることによって値が変化可能になっている</span>
<span class="token keyword def">class</span> hoge<span class="token punctuation">{</span>
<span class="token keyword">private</span> <span class="token keyword">int</span> foo<span class="token punctuation">;</span>
<span class="token keyword">public</span> <span class="token keyword">int</span> <span class="token function">Get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword this">this</span><span class="token punctuation">.</span>foo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">Set</span><span class="token punctuation">(</span><span class="token keyword">int</span> foo<span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword this">this</span><span class="token punctuation">.</span>foo <span class="token operator">=</span> foo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token function">hoge</span><span class="token punctuation">(</span><span class="token keyword">int</span> foo<span class="token punctuation">)</span><span class="token punctuation">{</span>
<span class="token keyword this">this</span><span class="token punctuation">.</span>foo <span class="token operator">=</span> foo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span>
このように値オブジェクトでクラスを書くと、オブジェクトを一度生成すれば、その後はオブジェクト内の値を意識しなくて良いため、状態管理が楽になります。