Javaとスレッドセーフ
前回、メモリリークについて取り上げました。 メモリリークはプログラマ個々人の意識と技量に依存した扱いが難しい問題であることについて触れました。
メモリリークと同様に難しいのがスレッドセーフです。
スレッドセーフを試験で確認するのはメモリリーク以上に困難です。 不可能といってもよいでしょう。 設計やコードレビューで防ぐしかありません。
スレッドセーフは複雑なトピックです。今回はスレッドセーフの基本的な考え方を取り上げます。
(それでも長文になってしまった。。。)
スレッドセーフとは
「スレッドセーフ」とはマルチスレッドプログラムの評価基準の一つです。
マルチスレッドプログラムの評価基準には「安全性」、「 生存性」、「再利用性」、「パフォーマンス」の4つの基準があります。
このうち「安全性」とはオブジェクトを壊さないことです。
複数のスレッドが利用しても安全性が保たれることを「スレッドセーフ」と言います。
具体的にはフィールドの値を壊さないことです。
加えて、あるスレッドが変更したフィールドの値が他のスレッドから見えることを保証することも安全性の重要な要件です。
Javaのメモリモデルではスレッドが実施した変更はすぐには共有メモリ(ヒープ)に反映されず、一旦、スレッドのキャッシュに反映されます。 このままでは他スレッドが変更内容を無視した処理を実施してしまいます。 スレッド間の可視性を保証することが重要になります。
レベル
世の中の全てのJavaクラスがスレッドセーフなわけではありません。
例えばJavaプログラマならみんな大好きなArrayListはスレッドセーフではありません。
またスレッドセーフの徹底度合いにもレベルがあります。
Effective Javaの項目82ではスレッドセーフのレベルを以下の4つに分類しています。
1. immutable
- immutableなクラスは変更できないのでスレッドセーフです。
- 呼び出し側のクラスでは同期を行わずにこのクラスを利用することができます。
2. unconditionally thread safe
- クラス内部で同期を行ってスレッドセーフにしているクラスです。
- 呼び出し側のクラスでは同期を行わずにこのクラスを利用することができます。
3. conditionally thread safe
- 一部のメソッドに関しては呼び出し側のクラスで同期する必要があるクラスです。
- どのメソッドにはどのような同期が必要か文書化されています。
4. not thread safe
- 全般的に呼び出し側のクラスで同期する必要があるクラスです。
- 同期の方法が文書化されています。
5. thread hostile
- 呼び出し側のクラスで同期しても使用することが危険なクラスです。
- 意図して作成されることはなく、プログラマのミスで生まれます。
- 発見した場合は修正するか破棄する必要があります。
スレッドセーフを意識しなくていいとき
プログラミングやコードレビューをしていて、どのようなときにスレッドセーフを意識すべきでしょうか?
まず最初にスレッドセーフを意識しなくていいときから見ていきましょう。
こちらの方が理解しやすいと思います。
1. 非マルチスレッド環境である
非マルチスレッド環境でクラスをスレッドセーフにしても危険なことはありません。しかし性能が犠牲になります。また工数も増えます。
システム開発のリソースは有限ですから、「とりあえず」スレッドセーフにする必要はないと思います。
2. 複数のスレッドからアクセスされるShared Resourceではない
マルチスレッド環境のすべてのオブジェクトが複数のスレッドからアクセスされるわけではありません。むしろ限られたオブジェクトのみ共有されるように設計するでしょう。
設計上、複数のスレッドからアクセスされないオブジェクトについてはスレッドセーフにする必要はないと思います。
3. Immutableである
クラスがImmutableであれば既にスレッドセーフです。
クラスをImmutableにするのは色々な意味でよいプラクティスです。Immutableにする努力は環境を問わず実施すべきだと思います。
4. スレッドがローカル変数のみ利用している
ローカル変数はスレッド固有です。他スレッドと共有されることはないためスレッドセーフです。
ただしメソッド引数でShared Resourceオブジェクトへの参照を受け取っている場合、そのオブジェクトへの読み書きに注意が必要です。そのオブジェクトが他のスレッドと共有されていて、かつスレッドセーフでない場合は同期処理が必要になります。
5. Thread Local
ローカル変数と同じです。
6. フレームワークがスレッドの独立性を保証している
フレームワークがスレッドを管理し、独立性を保証していることがあります。その場合は考慮は不要です。
フレームワークが独立性を保証するために規則や制約を課していることがあります。もしある場合は守りましょう。
スレッドセーフを意識すべきとき
スレッドセーフを意識すべきときを見ていきましょう。
1. マルチスレッド環境で、かつ複数のスレッドからアクセスされるShared Resourceがあり、かつMutableである
「スレッドセーフを意識しなくていいとき」の反対です。
2. データが壊れる箇所がある
前述の1の条件を見たすMutableなクラスでも安全性が脅かされる箇所(データ壊れる可能性がある箇所)がなければスレッドセーフです。
「データが壊れる箇所」とはAtomicではない操作をしているところです。Atomicではない操作は処理中に他のスレッドが途中で割り込んできてデータが壊れる可能性があります。
あるメソッドの中で2つ以上のフィールドを操作/参照している
- それぞれのフィールドに別々のスレッドが書き込んだ値がセットされる可能性がある
long、doubleを操作している
インクリメント演算子やデクリメント演算子を使っている
- 「++」や「--」はAtomicではない
3. スレッドセーフでないAPIを利用している
JavaのCollectionの多くは性能を優先してスレッドセーフな実装になっていません。例えばLinkedListやArrayListがそうです。これらのCollectionはデータの安全性が失われると例外を送出します。しかしそれに頼ったコーディングをするのは危険です。
他にもAPIドキュメントにスレッドセーフでないことが記載されているAPIを利用している場合には考慮が必要です。
4. あるスレッドの操作結果を他スレッドから見たいとき
Javaのメモリモデルではスレッドが実施した変更はすぐには共有メモリ(ヒープ)に反映されず、一旦、スレッドのキャッシュに反映されるのは前述した通りです。
キャッシュの内容がヒープに反映される契機を作らないと永久にスレッドの変更は他のスレッドから見えません。
フィールドをvolatileにするか、フィールド操作をsynchronizedで囲むことで他のスレッドがフィールドへの変更を見えることを保証できます。
これはint型などでも同様です。int型への操作、参照はatomicなので可視性も担保されていると誤解されていることがありますが、AtomicであることとVisibleであることは別です。
5. スレッドセーフな親クラスのメソッドをオーバーライドしているとき
スレッドセーフなメソッドをスレッドアンセーフなメソッドでオーバーライドするとデータが破壊されてしまいます。
スレッドセーフなクラスの継承には危険が伴います。メソッドをオーバーライドするときは考慮が必要です。
6. 具体例
- ドメインオブジェクト
- データオブジェクト
- データ構造を表すオブジェクト
- 要求やコールバックの登録を受け付けるオブジェクト
- WebアプリケーションのControllerクラス
対処
スレッドセーフでない箇所を見つけた場合、どうすればよいでしょう?
1. atomicパッケージ、スレッドセーフなCollectionの利用を検討する
atomicパッケージ、スレッドセーフなCollectionを利用することでsynchronizedやvolatileを使用せずにスレッドセーフにできることがあります。
2. synchronizedで同期する
synchronizedで同期します。
Shared Resource内部で同期する方式と、Shared Resourceを利用する側で同期する方式があります。
Efffective Javaによると利用する側で同期することが基本方針です。Shared Resource内部で同期する方が高い並行性が実現できる場合にのみそれを実施します。
同期は性能に影響します。synchronizedによる同期は必要な箇所に最小限の範囲で適用しましょう。
3. volatileで可視化する
排他制御が不要で他スレッドに対して可視化だけしたい場合はvolatileを利用できます。
フィールドに対してAtomicでない操作を行っている箇所がないか注意深く確認してからvolatileを適用してください。
その他の細かい話
ここまでの章に収まらなかった雑多なトピックをここにまとめます。
synchronized、wait, notifyを避ける
「Effective Java」や「Optimizing Java」などを読んでいるとJavaの方向性としてsynchronized、wait, notifyを使わないコーディングが主流になっていくそうです。atomic、concurrent Collection、synchronizerなどの使い方に慣れる努力をした方がよいようです。
synchronizedブロックの中でオバーライドされたメソッドやコールバックを呼ばない
設計時に意図していなかった例外、デッドロック、データ破壊が起こる可能性があります。
synchronizedブロックの中では最低限の処理しかしないことです。
ロック対象に注意
conditionally thread safeやnot thread safeなShared Resourceクラスを実装する場合、呼び出し側クラスはsynchronizedによる同期を行うためにロックを必要とします。ロックを外部に公開し、APIドキュメントにも明記してください。
unconditionally thread safeなクラスを実装する場合、呼び出し側クラスは同期を行う必要がありません。ロックを公開する必要がないので、外部から取得できないprivateなものにしましょう。不要なデッドロックを防ぐことができます。
呼び出し側クラスを実装する場合、間違ったオブジェクトにロックをかけないことです。スレッドセーフではなくなります。当たり前のことのようですがCollectionの中には「え?そっちをロックするの?」となるものもあります。APIドキュメントに明記されているのでちゃんと見ましょう。
スレッドセーフなデバッグメッセージ
デバッグメッセージがスレッドセーフでないと出力処理中にもデータが更新されて、出力結果自体が信頼できなくなり問題の解析ができなくなります。
Collections.synchronizedLlistでもイテレータは同期が必要
ArrayListはスレッドセーフではありませんが、Collections.synchronizedLlistでスレッドセーフなリストに変換できます。
add、removeをsynchronizedで囲む必要はなくなります。
しかし拡張for文でイテレータを使う場合はsynchronizedで囲む必要があります。
同期メソッドからのみ呼ばれるprivateメソッドの同期は不要
やってもいいですが性能が悪くなります。他から呼ばれないことが保証されているのであれば過剰な同期は避けた方がよいです。
デッドロック
ロックを取得する箇所が複数あるとデッドロックが発生する可能性があり、考慮が必要になります。
対策としてロックを取得する順序を統一することがよく勧められていますが、システムの要件が複雑になるとそれも難しくなります。
そのような場合は処理の起点、ルートとなるオブジェクトを一つ決めて、そのオブジェクトを最初に必ず取得するようにします。規則がシンプルな分、守りやすくなります。
コンストラクタの中からthisを漏らさない
コンストラクタ内からフィールドにthisを設定するとコンストラクタ完了前のオブジェクトが参照できてしまいます。
finalなフィールドはコンストラクタ完了後に可視化される仕様です。 よってコンストラクタ完了前のオブジェクトにアクセスすると壊れたデータにアクセスしてしまう可能性があり危険です。
もしフィールドにthisを設定する要件がある場合は、コンストラクタが終了してからにするよう配慮する必要があります。
Javaとメモリリーク
メモリリークは検出するのが難しい問題です。
プロファイラなどの助けを借りることも大事ですが、最大の防御線はプログラマ個々人の意識です。
Javaとメモリ管理
Javaは基本的にメモリを意識しなくてよい言語です。
どこからも参照されなくなったオブジェクトはGarbage Colllectorが自動で解放してくれます。
しかし全くメモリを意識しなくてよいわけではありません。
メモリリークの例
生成コストの高いオブジェクトをキャッシュして再利用することにしたとします。 しかしキャッシュには注意が必要です。
キャッシュ=オブジェクトへの参照をずっと保持する=ずっとGCされない、ということになります。 このような状態が意図せずに長期間継続した場合、メモリリークになります。
キャッシュを実装するときには、いつどのようにオブジェクトを解放するか注意が必要です。
ではプログラマは具体的になにを気をつければよいのでしょうか。
指針
全Javaプログラマの指針といえば「Effective Java」です。
項目7にメモリリークの詳細と具体例が記載されています。
この本は全体的に読みにくい本ですがこの項目に関してはよいサンプルコードが掲載されていて理解しやすいです。
プログラマとしてはここに記載されている内容を理解した上でコーディング、コードレビューに望みたいものです。
以下に骨子のみまとめます。
メモリリークの原因
- 使われなくなった参照が原因
- 参照が残っているとGCは使っていないことを判断できない
- 使われなくなった参照の廃棄方法には2種類ある
- 最も狭いスコープで利用してスコープの外に出す
- nullをセットする
- 基本的に1の方法を取る
- 2は例外的措置でやむを得ない場合にのみ実施する
(筆者感想) 1が徹底できれば楽ですがDIするためにオブジェクトをローカル変数で保持せず、フィールドで保持するケースは多いと思います。 DIが好きな人は2の可能性を常に意識する必要があります。
対策
- メモリリークの検出は難しい
- 試験で検出されることは稀で、Production環境でもすぐに顕在化するわけではない
- コードレビューやプロファイラに頼らざるを得ない
- 後工程で検出されることに期待するのではなく、プログラマ自身が気をつけて予防することが重要
メモリリーク注意すべきケース
1. 独自のメモリ管理をしている
- Stack、Queueなどを実装したとき
- 要素を解放してもnullをセットしない限り、GCの対象にならない
- 対策として要素が解放されるときは明示的にnullにセットする
2. オブジェクトのキャッシュ
- リークする理由は前述の通り
- キャッシュからオブジェクトを削除する方法には3つある
- WeakHashMapを使う(外部からのキーへの参照がなくなると自動削除される)
- バックグラウンド監視スレッドで一定時間で削除
- 追加時の副作用として古いものを消す
3. リスナー、コールバックの登録
- 登録しっぱなしで削除手続きがないとリークする
- 対策としてWeakHashMapを使う
Webフレームワークにおける開発の考察
リスクに直面する機会は少ない
一般的なWebフレームワークなどで開発している場合、意識する機会は少ないかもしれません。
リクエストやセッションごとに生成、破棄されるライフサイクルが短いオブジェクトはあまり神経質にならなくても大丈夫だと思います。 それらのオブジェクトの中で参照を解放することを忘れたとしても一定期間経過すればそれらオブジェクトは破棄され、そのオブジェクトが参照していたオブジェクトは参照を失いGCの対象になるからです。
Servletのように再利用される、長いライフサイクルを持つオブジェクトはフィールドに保持している参照に注意が必要です。 しかしスレッドセーフにするために可変フィールドをServletから排除することはよく知られたプラクティスであり、そもそもフィールド自体ないことも少なくないと思います。
しかし落とし穴はあります。
落とし穴1:ライフサイクルの長いオブジェクトのフィールド
例えばServletのフィールドにオブジェクトへの参照を持っていた場合、そのオブジェクトはGCされません。 フィールドが可変でもFinalでも関係ありません。
そのオブジェクトが更に別のオブジェクトを参照していて、そこに使われなくなった参照が蓄積されていたらメモリリークになります。
Webフレームワークでもライフサイクルの長いオブジェクトのフィールドには注意を払う必要があります。
落とし穴2:staticフィールドや定数
staticフィールドや定数がGC対象にならないことは直感的に理解できると思います。これらはGC rootとみなされ、GC rootが参照しているオブジェクトはGCの対象になりません。それだけでなく、そのオブジェクトが参照しているオブジェクトも同様にGC対象にならない点に注意が必要です。
staticフィールドや定数で参照を保持する場合、参照先のオブジェクトに使われなくなった参照が蓄積しないか注意が必要です。蓄積される場合は不要になった時点でnullをセットしましょう。
参照の循環は問題ない
参照が循環した場合、参照が永久に保持されてGC対象にならずメモリリークになると考えるかもしれません。しかしJavaでは大丈夫です。
GCにも色々な方式があり、Reference Countという方式では参照が循環するとメモリリークが発生します。
しかしJVMで採用されている方式でGC rootと呼ばれる参照の起点から辿れるかどうかでGC要否を判断します。GC rootから辿れなければ参照が循環していても使われなくなった参照と判断されてGC対象になります。
GC rootとはなにか?とは難しいトピックです。正確に理解するのは困難ですが大雑把に以下のように理解しておけばよいと思います。
- 実行中のスレッド(例: mainスレッド)のローカル変数
- 実行中のスレッドのThread Local
- staticフィールド
- 定数
結論としては、staticや定数でない限り実行中のスレッドで不要になったインスタンスはGC対象になるのでオブジェクト間の相互参照に注意する必要はありません。
結論
- 参照を蓄積する箇所があれば、参照がいつ削除されるか確認する
- staticや定数ではないのである処理が完了するとローカルスコープを抜けて参照が削除される
- ローカルスコープを抜けることはないが参照を明示的に削除する機構がある
- 参照が削除されるタイミングがなければ明示的にnullをセットする
TDDとファクトリ系のデザインパターン
ファクトリ関連のデザインパターンに関するポエムです。
まともな技術情報は含まれていません。
ファクトリ系のデザインパターン
ファクトリ系のデザインパターンには複数種類があります。
GOFのデザインパターン本で紹介されているのは下記2つです。
- Factory Methodパターン
- Abstract Factoryパターン
さらにGOFでは紹介されていませんが、より原始的なSimple Factoryパターンもあります。これは上記2パターンのような継承やメタモーフィズムを使わないシンプルなファクトリクラスを使うパターンです。
ファクトリは頻繁に使われるデザインパターンの1つで、プログラマ同士の会話でも「ここはファクトリを使って、、、」みたいな会話はよくあります。
でも私、ファクトリ系デザインパターンを全然、使ってないんですよね。。。
だから会話に入れなくて。。。
使い所がわかってない
私はコーディングするときは事前に分析&設計することはせず、テストが設計を主導するように意識してます。え?ティっ、、TDDの練習です(震え声)。
流れとしては、とりあえずTDDで書き始めます。そしてテストが進んでいく中で試験容易性、DRY原則やSOLID原則等の原則を意識したリファクタリングを実施します。その結果、デザインパターン的なものが生まれることもあります。しかし意識的にデザインパターンを適用することは基本的にしません。
スキルが低く視野の狭い人間が下手に事前設計してデザインパターンを適用するよりテスト主導で設計原則を重視して書いていった方が「マシ」なものができると経験上、思うからです。
この流れでコーディングしているとインスタンスの生成に関しては、呼び出し元でnewで生成して必要な場所にコンストラクタインジェクションで注入する、というワンパターンなやり方でいつも事足りてしまいます。
MainやControllerなど根っこのクラスは注入するオブジェクト数が多くてコンストラクタの引数が多いなー、とか、newしてるからテストやりずらいなー、と感じることはあります。でも根っこのクラスはそもそもテストしずらいものだと思ってますし、一度書いたら終わりなことが多いので、「なにかデザインパターンを適用して改善できるか検討してみよう」という流れになったことは今までありませんでした。
でも私も「ここはファクトリーで」とか言ってみたい!
ということで今回、時間を作って考えてみました。
考えてみよう
テスト主導な設計においてファクトリ系のデザインパターンの適用を検討するタイミングはいつ訪れるのでしょう?
またテストが進んでいく中でどのように各ファクトリ系パターンに発展していくのでしょう?
以降は簡易な実験的なコードでSimple Factory、Factory Method、Abstract Factoryを使ってTDDしてみた感想です。
どこでSimple Factoryパターンを適用しようと思うか?
どのファクトリ系パターンに発展する(or しない)にしてもまずは出発点はSimple Factoryパターンの適用から始まると思います。
ではどのようなタイミングでSimple Factoryパターンを適用する流れになるのでしょう?
デザインパターンを意識してなければ前述のワンパターンでほとんど事足りてしまいます。事足りてるのに適用するのはテスト主導とは言えません。不満が出て初めてその解決策としてSimple Factoryパターンを適用できます。
唯一、不満を感じるのは根っこクラスから注入するオブジェクトが増えてコンストラクタのシグネチャが大きくなったときです。
代わりに複数のオブジェクトを生成するファクトリオブジェクトを一つだけインジェクトするように根っこクラスを変更したらコードがすっきりしました。
興味深かったのはテストでモックしたときです。
テストコードでは継承&オーバーライドでモックしました。プロダクションコードだけ見るとSimple Factoryですが、テストコードも含めて見るとFactory Methodっぽくなりました。
また階層の境界をまたいでSimple Factorパターンを適用したときは境界なのでInterfaceを用意しました。プロダクションコードだけ見るとInterface付きのSimple Factoryですが、テストコードも含めて見るとAbstract Factoryっぽくなりました。
根っこクラスからnewが排除されました。Mockライブラリを使えばファクトリをMockへ置き換えることは可能です。ただ自分はあまりMockライブラリを使わないのでそれで根っこクラスを試験しやすくなったとは感じませんでした。
Factory MethodやAbstract Factoryへの発展
前述のテストコードの件を体験して感じたのは、要件次第で自然にFactory MethodやAbstract Factoryへ発展していくということです。
今までと同じで特にデザインパターンを意識せず、要件とテストと原則に任せればよいと思いました。
感想
今までファクトリ系デザインパターンとご縁がなかったのはテスト主導だからではなく、「コンストラクタの引数が多い」というコードスメルを放置していたからだと思いました。
他にも気づかずに放置しているコードスメルがありそうです。
Singletonパターンは使わなくてもいい?
Singletonパターンに関するポエムです。
長文です。
Singletonパターン
GOFデザインパターンの中でも最もわかりやすのがSingletonパターンです。
初心者が一番理解しやすいパターンだと思います。
私自身、初めてデザインパターンの本を読んだとき、唯一理解できたのがSingletonパターンでした。
疑問
そんな思い出深いSingletonパターンでですが実践においては使ったことがなく、どういう場面で使えばよいのかずっと疑問でした。
そんなに大きなメリットある?
「システムに一つしかないことを保証したいリソースを抽象化、管理するためにSingletonにする」という指針はよく耳にします。これはある意味では納得します。一方でそれは一般的に悪とされるグローバル変数です。グローバル変数化するデメリットをSingleton化するメリットが上回るのでしょうが、そのような比較検討を見たことがありません。
Singletonには超すごーいメリットがあるのでしょうか?それともグローバル変数って巷で言われているほど悪くはないのでしょうか?
他の選択肢に対する優位性は?
「システムに一つしかないことを保証したいリソースを抽象化、管理する」ならSingleton以外の実現方式もあります。例えばスレッドプールのように決まった数のリソースをプールして要求に応じて払い出すようなクラスを作成すれば実現できます。この方が汎用性は高いと思います。複数ある実現方式の中でなぜSingletonじゃないといけないのでしょうか?
「システム内に1つで十分なクラス」ってほとんどそうじゃない?
「システム内に1つで十分なクラスはSingletonにする」という指針もよく目にします。これを徹底するとシステム内の大多数のクラスがSingletonになる気がします。「複数あっても問題ないけど、このシステムの要件だと1つで十分」というクラスは相当数あるはずです。
でもSingletonだらけのシステムなんて見たことがありません(ネットの噂だと存在するらしいですが)。
それにそんな風に作られたシステムのコードの再利用性は低く、設計としていかがなものか思います。
どこまでSingleton化するか線引するためのもう一段階細かい基準があるのだと思うのですがそれがわかりません。
SOLID原則から見ると歪んでない?
私の行動指針であるSOLID原則の1つ、Single Responsibility Principleに照らし合わせるとSingletonパターンはやりすぎに見えます。あるリソースがあったとき、そのリソースを1つだけ使いたいアクターもいれば複数使いたいアクターもいるはずです。前述のスレッドプールのように責務を分割しないと片方のアクターの要求がもう片方のアクターに影響を及ぼす設計になります。
Singletonを強制するユニットテストはあるか?
私はTDDっぽくテストから設計を導出する(事前に設計するスキルがないから)のですが、今まで一度もSingletonパターンを使ったことがありません。
私のTDDのやり方が間違っている可能性はあります。脱線しそうですが多分、間違ってます。でもSingletonパターン以外の実装選択肢がないテストケースってどうやったらTDDの流れの中で出てくるんでしょうか?
私の拙い経験では他の選択肢を排除できず、SRPの観点からSingleton以外の方式を選択してました。予めSingletonを使うと決めてないと無理じゃないかと思ったりします。
その割に周りでよく耳にするのはなぜ?
決定的とは言えないまでもマイナス要素ばかりが目について、逆に積極的に採用する理由を見いだせないのですが、「ここSingletonにする?」みたいなプログラマー同士の会話はよく耳にします。特にファクトリー周りの話ではよく出てくる気がします。私だけが気づいていないすごーいメリットがSingletonにはあるのでしょうか?全然、会話に入っていけません。
その割に周りでよく目にするのはなぜ?
コード上でも時折目にします。その度にSingleton化せざるを得なかった理由を足りない脳みそで考えるのですがなにも思いつきません。
私だけがアホなの?
確かにアホです。でも私だけがわかっていないSingletonのいい所ってなんなんでしょう?皆さん、どうやってSingletonの適用要否を判断してるんでしょう?
参考になる記事発見
なにか(忘れた)調べてたら偶然、この記事を見つけました。
Singletonパターンの悪いところが割と具体的に書かれてます。
長い間、抱えてきた疑問が解消する期待が膨らんで参考サイトまで含めて何度も熟読してしまいました。
これらを読んで得た感想としては、「Singletonパターンが必須になる状況ってないんだな」ということです。
考察
そんなに大きなメリットある?
グローバル変数化に対する純粋な正当性はないっぽいですね。
プログラマ個々人の価値観としてグローバル変数を便利な必要悪として許容するタイプか、絶対拒絶するタイプかで判断が変わるだけのようです。
他の選択肢に対する優位性は?
これもないっぽいですね。
設計原則やTDDを重視する人たちはSRPやTDDとの相性からむしろSingleton以外の選択肢を推奨してます。
「システム内に1つで十分なクラス」ってほとんどそうじゃない?
Singletonを利用する人の中でも「システム内に1つで十分なクラス」をどこまでSingleton化するかはわかれるようです。本当に大半のクラスをSingleton化してしまう人もいれば、著名なパターン本でSingletonの利用が推奨されている範囲内に留める人もいます。プログラマー個々人の属人的な判断になっているのが実情のようです。
属人的な判断をする上での共通的な指針すら揺らいでいる状況です。GOFのエリック・ガンマは「Singletonパターンをデザインパターンから取り除きたい」と後悔しているそうです。これをもってGOFのデザインパターン本のSingletonパターンの記述は無効だと考える人たちもいます。
それでも書籍通りに解釈して広い範囲で利用するか、著名なパターン本で明示的に強く推奨されている範囲内に留めるか、使わないか、完全に個々人の価値観に委ねられています。
SOLID原則から見ると歪んでない?
責務を分割すべきだと主張している人も多いです。
Singletonを強制するユニットテストはあるか?
ここについてはまだ答えを得られてません。
後述しますがユニットテストとの相性が悪いので、TDDの中では意識的にSingletonを避ける人もいるようです。
その割に周りでよく耳にするのはなぜ?
やはり著名な本に記載されているからなのかなと思いました。 例えばGOFのデザインパターン本のAbstract Factoryの項では以下のように記載されています。
典型的なアプリケーションでは、部品の集合ごとにConcreteFactoryクラスのインスタンスを1つしか必要としない。したがって、通常ではConcreteFactoryクラスをSingletonパターンを使って実装することが最良の方法になる。
エリック・ガンマのコメントを知っていたとしても、全てのプログラマの聖典にこのように明記されていたら一応、相手に確認して意識合わせするのが自然な流れだと思います。
エンタープライズシステム関連の著名なデザインパターン本やアーキテクチャパターン本でもSingletonパターンと組み合わせることを勧めていることがあります。もしそのような書籍からパターンを採用する場合もSingleton化について意識合わせするのは自然な流れだと思います。
その割に周りでよく目にするのはなぜ?
その意識合わせの議論の中に、Singleton肯定派がいて、Singleton否定派が戦わなくて、Singleton化することが採択された結果なのでしょう。著名な本に書かれているという事実は大きいです。「長いものには巻かれろ」は人間の普遍的心理です。
もしくは議論自体なく、実装者の一存で実装されてコードレビューで見つけたけど、今更指摘しても遅いからとスルーされたのかもしれません。昨今のアジャイルな流れの中では事前にコーディング規約などで禁止しておかないとSingletonは防げないと思います。しかしそこまでしているプロジェクトはほとんどないのではないでしょうか。
その他の批判
マルチスレッド環境ではアクセス管理(同期)が必要で、実装負荷が増えます。
ユニットテストと相性が悪いと言われています。Singletonのフィールドの値はテスト間でクリアされないため先発のテスト内容が後続のテストに影響を与えてしまうからです。これはSingletonだけでなくSingletonを利用するクラスのテストも難しくします。フィールドの初期化処理をいれれば解決しますが、そこまでするメリットがないと感じるようです。
クラス名さえ知っていればどこからでも呼べるため、クラスの階層設計を無視してどこからでも呼び出せる飛び道具になります。多用するとクラス間の依存関係がぐちゃぐちゃになってしまいます。これはSingletonだけでなくStaticメソッドを提供しているクラス全般に言えることです。クラスは与えられた(Injectされた)クラス以外は利用すべきではないと考える人にとっては悪です。
感想
「使ってもいいけどSingletonパターンが必須不可欠になる状況はない」と感じました。使いたくなければ使わなくも全然、大丈夫っぽいです。
一方、「著名なXXX本にこのパターンとの組み合わせが推奨されているから」という理由で使う人たちがいるということを認識する必要があると感じました。
使いどころ
逆にSingletonパターンを使ってよい状況というのはどういう状況でしょう?
こんな指針を見つけました。
状態を持たないこと
- 状態を持つとグローバル変数化する
- 「システムに一つしかないDBセッションをSingletonで管理」とかはNG例
ポリモーフィズムが絡む (抽象クラスまたはインタフェースを実装している) こと
- Interfaceを実装せず、状態も持たないならただのUtilityクラスでよい
- Interfaceを継承する子クラスの中で状態を持たないクラスはSingletonにする
- Interfaceを継承する子クラスの中で状態を持つクラスは普通のクラスにする
SingletonはInjectionする
利用メソッド側でgetInstanceをハードコードすると試験容易性が落ちる
適したケース
- Strategyパターンとは相性がよい
- Strategyの実装クラスは状態を持たない
その他の適したケース(怪しい)
- ロギング
- キャッシュ管理
- スレッドプール管理
- データベース接続ドライバ
- ソケット制御ドライバ
2021年にPython 2.7を書いてみた
正確にはPython 3.9で書いたコードをPython 2.7に変更しました。
そのとき感じたことを記録します。
はじまり
作業効率化のために簡単なツールをPythonで作ってみました。
自分の環境では問題なく動作したのですが実環境にうつしたらエラーが発生。
pathlibがないって?
恐る恐るPythonのバージョンを確認したら、、、なんと2.7でした。
Python 2.7へ書き換え
諸事情により実環境のPythonのアップデートは不可でした。
もう諦めようかとも思いましたが時間もあったのでコードを2.7へ書き換えることにしました。
まずPyCharmでインタプリタをPython 2.7へ変更します。
おおおお、、、、、大量のエラーが。。。
その1:pathlibがない
これはosに置き換えればよいので簡単なお仕事でした。
その2:type hintがない
type hintがあるとIDEが補完を出しやすくなり、作業効率や安心感が増します。お勧めです。
今回は3.9で書いてから2.7化したのでtype hintの恩恵を受けながらスムーズにコーディングできました。
しかし2.7ではtype hintは使えません。
書き換えること自体は簡単な作業でしたが、最初から2.7でtype hintなしでスクラッチからコーディングしてたらと思うとぞっとしました。
2.7とは仲良く慣れないと思いました。
その3:f Stringがない
str.formatに置き換えます。
f Stringの方がコード全体がすっきりして好みです。
その4:コンストラクタを疑似private化した部分がエラー
Builderパターンを適用したためコンストラクタからインスタンスを生成してほしくないケースがありました。
Pythonではメソッドをprivateにすることはできません。そこで__new__を殺して、__init__を呼ぶgetInstanceクラスメソッドを作ってました。
3.9では問題なく動いたのですがなぜか2.7ではエラーが出ます。
エラーの詳細はよくわかりません。
2021年にもなって2.7の詳細について学びたくもないので__new__を復活させるという暴挙に出ました。
新たな問題を生んだわけですが、とりあえずエラーは消えました。
誰も保守、運用しないコードだからいいでしょ。(言い訳)
結論
単体試験も含めて2.7化するのに1時間くらいかかりました。
基本的な標準ライブラリしか使わない簡単なコードだったのでシンプルな書き換えで済みました。
外部ライブラリを色々使う、複雑なアプリだったらカオスだったと思います。
それでもPython 2.7が嫌いになるには十分でした。
願わくばいつも新しい環境でコーディングしたいものです。
Python 2.7とか、Cenots7とか、そろそろやめてください。
例外処理の設計
例外処理の構造について調べる機会があり、以下の3点について考えてみました。
- 例外が発生した場合の処理の仕方
- エラーを検知したときの例外の発生させ方
- 例外のcatchの仕方
- 階層をまたぐ例外
1. 例外が発生した場合の処理の仕方
呼び出したメソッドが例外を発生させた場合、どうすればよいか考えます。
1.1 共通
- まずはどのように元の状態に戻せるか考える。
- 「元の状態」= メソッドを呼び出す前の状態。
- 次のアクションはその例外が回復可能か、回復不能かで異なる。
- 対処がわからないときは上に丸投げ(throws)が最低ライン
1.2 回復可能な場合
- catchして回復を試みる
- 再接続、Fallback、切り替えなど
1.3 回復不可能な場合
- 業務エラーか技術的なエラーかで対応が異なる
業務エラー
- 業務ロジックの代替パスの一部
- catchして代替パスに切り替える
- エラー画面表示、再入力画面表示、再ログイン画面表示など
技術的なエラー
- 上位に伝搬させて上位層に適切にプログラムを終了させる
- catchした上位層はログ、管理者通知、ユーザ通知、ロールバック、リソース開放などを実施する
- 通常、これらの処理は上位層で共通化されている
- 下位の業務ロジック内で個別に実装しない方が見通しがよくなる
- 検査例外、非検査例外の両方がありえる
1.4 注意
- 無視しない
- 例外にはその場で処理すべきものと上位が処理すべきものがあることを理解する
- 上位に伝搬すべき例外を止めない
- その場で処理すべき例外のcatch漏れを防ぐ
- catch Exceptionしない
- なにが発生するか読み取れない(可読性低下)
- 上位に伝搬すべき例外(特に非検査例外)まで止まってしまい、例外設計が崩れる(想定外動作)
- throws Exceptionしない
- その場で処理すべき別の例外をcatch漏れしていてもコンパイラが気づけない(想定外動作)
- 上位層への伝搬を全て止める親クラスを書かない
- 実装ミスがあっても決して死なない不死身の子クラスが誕生する
- 問題発生の検知や切り分けが難しくなる(運用性低下)
- 上位へ伝搬させる例外をe.printStackTraceしない
- 伝搬先の上位での出力処理と重複して混乱する(運用性低下)
- 入門書などでよく見かけるので初心者がやりがちなミス
2. エラーを検知したときの例外の発生させ方
エラーを検知したとき例外を発生させたいときがあります。 どのように発生させればよいか考えます。
2.1 呼び出し側が防げるエラーの場合
- プログラミングエラーとも呼ばれる
- 呼び出し側はtry/cachによる事後対処ではなく、事前のチェックで防ぐべき
- よって非検査例外をスローする
2.2 呼び出し側が防げないエラーの場合
- 呼び出し側が万全を期しても発生を防げない=呼び出し側でtry/catchによる事後対処が必要
- よって検査例外をスローする
- しかし検査例外について賛否があり、案件ごとに関係者で意識をあわせる
- (批判)新しい検査例外は伝搬経路上のメソッドのthrowsやcatchに追加される必要がある=Open Closed Principle違反
- (利点)try/catchが必要な例外を利用者に確実に伝えることができる
2.3 発生させる例外クラス
- Java標準の例外クラスを発生させるか、独自のアプリケーション例外を作成して発生させるか好みが分かれる
- アプリケーション例外は表現力、抽象力、バラエティーに勝るが数や種類が制御できなくなるリスクがある
- 案件ごとに関係者でルールを決める
3. 例外のcatchの仕方
例外とはエラーが発生したことを上位層へ伝搬させる仕組みなので、上位層のどこでcatchするかは重要なポイントです。
- 前述の通り回復可能な例外は発生元の近くでcatch&回復する
- 回復不能な例外のcatchは処理を呼び出す「根っこ」のクラスにのみに実装することを基本とする
- Baseクラス=Routing担当と業務ロジックの間にある業務ロジックの親クラス
- ログ出力などもBaseクラスにまとめることができる
- 最近ではFilterやAOPなどFrameworkの様々な機能を使える
- これにより末端クラス側をすっきりさせることができる
4. 階層をまたぐ例外
境界における例外は、抽象化することが重要です。
- 階層の境界で投げる検査例外は抽象化されたものにする
- パッケージ固有の例外だとパッケージを入れ替えるたびに対向側はcatchやthrowsを書き換えなくてはいけない=Open Closed Principle違反
- 下位層のパッケージが投げる検査例外は回復されることを期待しない
- 上位層が例外を回復できるということは上位層が下位層の詳細に依存しているとうこと
- 回復できるものは下位層内で回復するよう努力する必要がある
- これには例外もある
- 上位層がトランザクション管理をしている場合等
JavaScript ES6 + JestでTDD(fetch API Mock編)
前回、ブラウザ向けのJavaScript ES6コードにfetch APIを導入し、MockなしでTDDする方法を紹介しました。
今回は同じテストをMockで行う方法を紹介します。
なお今回はcar.js側の変更は一切ありません。変更されるのはテストだけです。
なぜMockするのか?
前回の繰り返しになりますがfetch APIをMockしない場合、デメリットが色々あります。
ちなみに個人的にはMockするときはMockライブラリを利用せず、自分でテスト用Mockを作成してInjectする方が好きです。
テスト1:データ取得成功
まずはデータ取得に成功したケースのテストを書き換えてみましょう。
carMock.test.js
前回はglobalなfetchをcar.test.js内でnode-fetchに上書きしました。
今回はglobalなfetchをMockで上書きします。
一つのjsファイル内でglobalなfetchを何度も上書きすると思ったような挙動にならないことがありました。そこでシンプルにファイルを分けることにします。
test/carMock.test.jsファイルを作成します。
carMock.test.jsに記載する単体試験は以下の通りです。
import Car from '../car'; global.fetch = jest.fn(); describe('CarMock', () => { const apiServer = 'https://reqres.in'; let car = null; beforeEach(() => { car = new Car(); global.fetch.mockClear(); }); <async/awaitの場合> it('should get traffic data by fetch mock 1', async () => { global.fetch.mockImplementationOnce(() => { return Promise.resolve({ ok: true, json: () => { return Promise.resolve({ data: { id: 2 } }) } }); }); const traffic = await car.getTrafficData(apiServer + '/api/user/2'); expect(traffic).toBe(2); expect(global.fetch).toHaveBeenCalledTimes(1); }); <Promise/thenの場合> it('should get traffic data by fetch mock 2', () => { global.fetch.mockImplementationOnce(() => { return Promise.resolve({ ok: true, json: () => { return Promise.resolve({ data: { id: 2 } }) } }); }); return car.getTrafficData(apiServer + '/api/user/2').then((traffic) => { expect(traffic).toBe(2); expect(global.fetch).toHaveBeenCalledTimes(1); }); });
大半は前回と同様です。変更した箇所だけ解説します。
「global.fetch = require('node-fetch')」は不要です。
「global.fetch = jest.fn();」でglobalなfetchをJestのMockで上書きしています。これでテスト実行時はcar.js内のfetchもMockに上書きされます。
「global.fetch.mockImplementationOnce」でテストごとのMockの挙動を定義しています。挙動を定義するにあたっては前回、Mockなしテストで得た知識を活用しました。resolveされたPromiseはokとjson関数を持っている必要があります。
Mockの挙動の定義は「global.fetch = jest.fn(() => { ... });」で行うこともできます。しかしこの方法だとテストごとに挙動を切り替えることができない場面があり、試行錯誤の結果、上記の形に落ち着きました。
「expect(global.fetch).toHaveBeenCalledXXX」で色々Spyできます。
呼び出し回数等のMock情報はテストごとにクリアされません。このためbeforeEachの中で「global.fetch.mockClear()」しています。
テスト2:エラー応答受信
サーバからエラー応答を受信したケースのテストを変更します。
carMock.test.js
carMock.test.jsに記載する単体試験は以下の通りです。
「global.fetch.mockImplementationOnce」でMockの挙動を変更しています。Mockは「ok = false」を持つresolveされたPromiseを返します。
<async/awaitの場合> it('should reject when 4XX is returned 1', async () => { global.fetch.mockImplementationOnce(() => { return Promise.resolve({ ok: false }) }); expect.assertions(1); try { await car.getTrafficData(apiServer + '/api/user/23'); } catch (error) { expect(error).toBe('Error 404'); } }); <Promise/thenの場合> it('should reject when 4XX is returned 2', () => { global.fetch.mockImplementationOnce(() => { return Promise.resolve({ ok: false }) }); return expect(car.getTrafficData(apiServer + '/api/user/23')).rejects.toBe('Error 404'); });
テスト3:fetch APIのエラー
fetch API自身の処理でエラーが発生したケースのテストを変更します。
carMock.test.js
carMock.test.jsに記載する単体試験は以下の通りです。
「global.fetch.mockImplementationOnce」でMockの挙動を変更しています。MockはrejectされたPromiseを返します。
<async/awaitの場合> it('should reject when error happend 1', async () => { global.fetch.mockImplementationOnce(() => { return Promise.reject({ message: 'error message', result: 'description of the error' }); }); expect.assertions(1); try { await car.getTrafficData('http://dont.exist.api/api/bogus'); } catch (error) { expect(error).toMatch('message'); } }); <Promise/thenの場合> it('should reject when error happend 2', () => { global.fetch.mockImplementationOnce(() => { return Promise.reject({ message: 'error message', result: 'description of the error' }); }); return expect(car.getTrafficData('http://dont.exist.api/api/bogus')).rejects.toMatch('message'); });