プログラミングには、シンプルでありながら強力な概念があり、広く活用されていません。それはイミュータビリティ(不変性)です。
基本的に、オブジェクトが一度作成された後にその状態が変化しない場合、そのオブジェクトはイミュータブルです。その結果、インスタンスがイミュータブルであるクラスはイミュータブルです。
イミュータブルオブジェクトの使用を支持する重要な論拠が1つあります。それは、並行プログラミングを劇的に簡素化するということです。考えてみてください。なぜ正しいマルチスレッドコードを書くことがそれほど難しいのでしょうか?スレッドのリソース(オブジェクトやその他のOSリソース)へのアクセスを同期させるのが難しいからです。なぜこのアクセスの同期が難しいのでしょうか?複数のスレッドが複数のオブジェクトに対して実行する複数の読み取りおよび書き込み操作間で競合状態が発生しないことを保証するのが難しいからです。書き込みアクセスがなくなったらどうでしょう?言い換えれば、スレッドがアクセスするオブジェクトの状態が変化しなかったらどうでしょう?もはや同期の必要はなくなります!
参照されるオブジェクトのクラスがイミュータブルであるのは、publicフィールドがなく、内部データを変更できるメソッドがなく、派生クラスのメソッドがその内部データを変更する方法もない場合です。そして、値が変化しないため、すべての場合に同じオブジェクトを参照できます。コピーコンストラクタや代入演算子は必要ありません。このため、コピーコンストラクタと代入演算子をprivateにするか、boost::noncopyableから継承するか、C++11の新機能である「明示的にデフォルト化および削除された特殊メンバー関数」を使用することが推奨されます。
C++クラスがイミュータブルになるように設計されていなくても、その型の変数にconstキーワードを追加すれば機能します。ただし、このソリューションにはいくつかの欠点があります。
- イミュータブルにしたい各変数宣言にconstを追加する必要があります。時々それを忘れると、マルチスレッドコンテキストで予期しないバグが発生します。
- 変数がconstとして宣言されていても、一部のフィールドがmutableとして宣言されている場合、constメソッド内でオブジェクトを変更できる可能性があります。そのような場合、オブジェクトの状態は依然として変更できるため、オブジェクトはミュータブルです。
例えば、Stringクラスは、C#、Java、Dなどの他のいくつかの言語でイミュータブルであることでよく知られています。文字列は本来広く使用されており、それらをイミュータブルにすることはマルチスレッド環境に特に適しています。C++では、std::stringはイミュータブルではないため、代替案はconstキーワードを使用することです。
ドメイン駆動設計とイミュータビリティ
ドメイン駆動設計は、2つの前提に基づくソフトウェア設計アプローチです。
- 複雑なドメイン設計はモデルに基づくべきであるということ、そして
- ほとんどのソフトウェアプロジェクトでは、主な焦点はドメインとドメインロジックに置くべきであるということ(システムを実装するために使用される特定のテクノロジーとは対照的に)。
言い換えれば、DDDの中心はモデルであり、開発を始める際に最初に行うべきことの1つはモデルを定義することです。作成するモデルと設計は互いに形成し合うべきです。モデルはビジネスドメインの知識を表現すべきです。
一般に、スレッド間で共有されるデータはモデルエンティティに関わるものであり、それらのエンティティをイミュータブルにすることは副作用の排除に役立ちます。Wes Dyerよりうまく言えないので、彼を引用します。
We all know that generally, it is not a good idea to use global variables. This is basically the extreme of exposing side-effects (the global scope). Many of the programmers who don’t use global variables don’t realize that the same principles apply to fields, properties, parameters, and variables on a more limited scale: don’t mutate them unless you have a good reason.(…)ユニットの信頼性を高める1つの方法は、副作用を排除することです。これにより、ユニットの合成と統合がはるかに容易かつ堅牢になります。副作用がないため、環境に関係なく常に同じように機能します。これは参照透過性と呼ばれます。
イミュータブルクラスのもう一つの利点は、リスコフの置換原則(LSP)に違反できないことです。Wikipediaページから引用したLSPの定義は次のとおりです。
Liskov’s notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).イミュータブルオブジェクトでは、状態は変化できないため、プロパティは変更できません。
C++ソースコードでイミュータブルクラスを検出する
イミュータビリティは、コンパイル時に強制できる機能です。言い換えれば、静的解析ツールによって強制できます。 CQLinqは、静的解析ツール CppDependに付属しており、型に適用されるIsImmutable条件を提供します。コードベース内のどの型がイミュータブルかを調べるには:
from t in Types where t.IsImmutable select t型に対するIsImmutable条件に加えて、2つの興味深い条件も使用できます。
ChangesObjectStateとChangesTypeStateです。その名前が示すように、ChangesObjectStateはクラスのインスタンスフィールドに代入するメソッドにマッチし、ChangesTypeStateはクラスのstaticフィールドに代入するメソッドにマッチします。
ChangesObjectState条件は、親型のインスタンスフィールドに代入するメソッドにマッチします。
ChangesTypeState条件は、親型のstaticフィールドに代入するメソッドにマッチします。
これら2つの条件により、プログラムの状態を変更できるメソッド、つまり副作用を引き起こすメソッドを簡単に特定できます。
副作用を引き起こさないメソッドは、通常、純粋メソッドと呼ばれます。CQLinqで純粋メソッドにマッチさせるには、次のように書けます。
from m in Methods
where !m. ChangesObjectState && !m. ChangesTypeState && !m.IsConstructor
select m
結論
副作用を排除することで、ユニットの合成と統合がはるかに容易かつ堅牢になり、マルチスレッドプログラミングが簡素化されます。イミュータブルクラスと純粋メソッドは、副作用を排除するための鍵です。
