Singleton
目的
Singletonの目的は一つだけインスタンスを生成し、それを使い回すことです。
パターンの実装としては「一つしか生成できなくする」ための制約を作り込む形となります。
OOPとインスタンス数
オブジェクト指向(OOP)の世界では、特別な理由がない限り現実世界のモノの数と、インスタンスの数を一致させます。豚が3匹いるなら
Pigインスタンスを 3つ生成します。同様に車が1000台ならCarインスタンスを1000個生成します。この理由は、複製したインスタンスの数だけ固有の情報を格納できるからです。現実世界のものが増えたのならば、それを管理するオブジェクトも同じだけ増やすのが基本です。オブジェクト指向言語が現実の世界や概念の構造を雄弁に表現できるのは、この状態の複製が簡単に(new Car()orCar{})実現できるからという点が大きいのです。さて、オブジェクトと言えば状態と振る舞いを持ちます。この両者で特に大きな進化をもたらしたのはどちらでしょうか?実は、インスタンスの増加で増えるのは状態だけです。状態はクラス(struct)定義時のメソッドの宣言から変わることはありません。インスタンスが複製されてもその振る舞いが同じ様にコピーされるだけです。唯一の振る舞いの変化が許される部分は、そのインスタンスの状態に依って振る舞いが変わるロジックが定義されている時です。しかし、オブジェクト指向言語によって主役の座に位置するのが状態であることに変わりはありません。
この常識を根底から覆すのが Singletonです。何故ならその状態を1つで良い…と言い張るのですから。しかし Singletonについて深く考察してゆくと、そこにもまた OOPの本質に迫る側面があることに気付くはずです。
一つだけインスタンスを生成してそれを使い回すと何が良いのでしょうか。すぐには思い浮かばないかも知れませんが、これが活きる局面は数多くあります。
1. 数の限定
最初に紹介するのは、本当に数が限られているものをそのとおりに表現するものです。1つしかない物を厳格に表現するなら Singletonは良い選択です。例えば日本に政府は一つしかありません。つまり以下のコードが成立します。
gov := jpgov.Instance()
ディズニーランド内にミッキーマウスは複数存在しないという話を聞いたことがあります。それが本当なら次のコードも成立します。
m := disney.MickyInstance()
次にシステム的な面で考えてみます。データベースを管理するマネージャが1つしかないのであれば、以下の表現が可能です。インスタンスは関数経由で取得しても良いですし、初期ロード時に生成しても構わないなら変数として用意しておいても良いでしょう。
// 関数で取得(実装上の選択肢は広がる)
dbMgr := database.ManagerInstance()
// 変数で取得
dbMgr := database.ManagerInstance
2. 状態の共有
Singletonが公開されるということは、単一の状態も公開されるということです。一般的にSingletonの参照はグローバルに提供されることが多いため、どこからでもアクセス可能な単一の状態という便利なものが手に入ります。例えば、Util系のクラス(package)は振る舞いという観点で単一のものを提供しますが、Singletonでは状態という観点で単一のものが提供出来るわけです。
次の例は、Observerパターンで通知を行う局面です。システムの各所からこのObserverに通知が入ります。このObserverは Singletonとして常に単一のインスタンスが参照されるので情報を集中的に管理できています。各所で別々のインスタンスを生成してしまったのでは集中管理になりません。
func (s Service) report() {
o := observer.Instance()
o.reportAccess(1)
}
一つしか無いというのは、一箇所で情報を管理できるという強みがあるのです。総理大臣や大統領はいくら忙しくても2名以上にしません。責務あるインスタンスに情報を集約させるための手段としても Singletonは有効なのです。
3. 参照の取得
上の例をもう少し使います。通常、Observerのインスタンスが必要なら上位の生成元から延々と引数で引き渡す必要があります。しかし、Singletonではその必要がありません。Singletonの生成部分はグローバルな関数(Javaなら staticメソッド, Golangなら グローバル関数)ですので、調達は簡単です。引数をシンプルに保てますのでその点では設計上の長所となります。
一方で、グローバルな関数に触れるというのは、その処理が副作用を持つということです。単純な長所だけではないので、この部分には気をつける必要があります。余談ですが、この解決策としてDI(Dependency Injection: 依存性の注入)という仕組みが存在し、DIが導入の際には Singletonは DIコンテナ側で実現されます。これがSingletonの完成形かもしれません。
type Service struct {
// 注入される依存性
o *observer.Observer
}
func (s Service) report() {
// 利用側から取得の責務が消滅
s.o.reportAccess(1)
}
生成と利用
Singletonの短所として「生成過程の混在」があります。利用側からすれば、唯一のインスタンスが利用できればそれで良いのですが、それを保証するためにSingletonを利用すると Singletonへの依存が発生してしまいます。これは インスタンス生成のために Factoryメソッドを利用するとFactoryメソッドへの依存性が発生してしまうのと同様です。問題はインスタンスを利用する局面において、生成する局面を把握してしまっている点です。エリック・エヴァンスの例で説明するならば、自動車工場の責務と自動車の責務は別であるべきということなのです。これに対する少しFatな解決策がDIというイメージです。
4. 軽量化
不要なインスタンス生成を避けるということは、その分の負荷を下げるということです。本来、複数生成はずのインスタンスを単数で対応させるということは、OOP的な原則からは逸脱しますが、それでもそれが有効な局面は存在します。デザイン・パターンでは Flyweightパターンがそれに該当します。Flyweightパターンは生成コストの高いリソースのインスタンスを使いまわし、処理の軽量化を図るパターンです。また、Enumの様に複数の種類を種類ごとに単一のインスタンスで表現する際にも単一のインスタンスは有効です。DDD的な発想をするならば、値それ自体を表現するValueObject (例えばJavaの java.lang.String, java.lang.Integer等)は、Singletonで表現可能です。
5. 同一性の確保
通常、同じ状態のインスタンスを2つ生成しても、属性値が同じなだけでポインタのアドレスは異なります。しかしSingletonではポインタのアドレスすら同じとなります。前者の状態を同値と呼び後者の状態を同一と呼びます。Javaならequals(Object)のみならず==で比較してtrueが返る状態を、Golangならreflect.DeepEqual(interface{}, interface{})のみならずポインタ同士を==で比較してtrueで返る状態を、それぞれ保証できるということです。
概要
先に述べたように、様々な目的で活用可能なのが Singletonですが、自分で実装する機会は少ないかも知れません。 理由は言語機能的に、またはF/W的に実現されることが多いパターンであるからです。 例えば Enumは Singletonの独自実装が可能なものの、言語がサポートしている場合が多いです。 また、DIコンテナの様に内部では単一インスタンス管理をしているものの、利用側は意識せずに利用しているはずです。
とは言え、生成インスタンスの数を制限する、特にSingletonの様に単一に絞る…というテクニックは一度覚えておくと様々な局面で利用可能 となりますので、身につけておくと良いと思います。
実現
実際に Singletonが動きを保証するテストケースを見てみます。次のテストケースでは、Singletonと非Singletonをそれぞれ取得し、同値性と同一性を確認しています。双方ともに同値性は保証できていますが、同一性はSingletonのみで保証されています。
// Singletonは唯一のインスタンスを用意し、それ以外を生成できなくするパターンです。
// そのため、唯一のインスタンスしか存在しないことを保証できます。
func TestSingleton(t *testing.T) {
// Instance()以外で生成できない => singleton.singleton{} は不可能
s1 := singleton.Instance()
s2 := singleton.Instance()
// インスタンスの内容が同じだけでなく
assert.Equal(t, s1, s2)
// インスタンスの実体も同じ
assert.True(t, s1 == s2)
}
// 逆に一般的な生成パターンは、コンストラクタ関数の中でインスタンスを生成します。
// そのため、同じ値を持ちますが(同値)、参照は同じ(同一)ではありません。
func TestNonSingleton(t *testing.T) {
s1 := non_singleton.New()
s2 := non_singleton.New()
// インスタンスの内容は同じだが
assert.Equal(t, s1, s2)
// インスタンスの実体は異なる
assert.False(t, s1 == s2)
}
次に、Singletonの作り方を説明します。Singleton構築の手順は次の3点です。
- 外部からインスタンスを生成できなくする
- Javaではコンストラクタを
privateにすることで実現 - Goではstructを(小文字で開始し)
package privateにすることで実現
- Javaではコンストラクタを
- インスタンス取得用の関数を別途用意し、そこでは常に同じインスタンスを返す様に実装する
package singleton
// Singletonの取得
func Instance() *singleton {
// 唯一のインスタンスの参照を返却
return instance
}
// 唯一のインスタンスを用意
var instance = &singleton{
Value: "Hello",
}
// 構造体の宣言
type singleton struct {
Value string
}
Golangでは privateなコンストラクタは存在しないため、Singleton用に独自のパケージを用意し、外部からアクセスさせることで外部からのインスタンス生成を阻害します。次に、これが非Singleton、つまり通常のクラス(struct)ではどうなるかを見てみます。
package non_singleton
// 通常の構造体の取得
func New() *nonSingle {
// 毎回生成して返却
return &nonSingle{
Value: "Hello",
}
}
// 通常の構造体の宣言
type nonSingle struct {
Value string
}
差分が見えたでしょうか。
まとめ
今回は、常に単一のインスタンスを返すSingletonパターンを学びました。Singletonは単一のクラス(struct)に閉じたコンパクトなパターンであるにも関わらず、様々なデザインで活用される奥深いパターンであることが理解できたのではないでしょうか。1を1として表現したり、Nを1で簡略化したり、値だけではなくアドレスまで同じにしてしまったり、Singletonの力を感じ取ることができたならば幸いです。
では、Singletonのあるプログラミング・ライフを楽しんでください。
comments powered by Disqus