※以下はobjc.io, Issue #13 Architecture, Avoiding Singleton Abuseの日本語訳です。
著者:Stephen Poletto
Singletonの不正使用をを回避すること
シングルトンは、Cocoaを通して使用されるコアのデザインパターンの一つです。実際には、 Appleの開発者向けライブラリは、シングルトンをCocoaに於ける重要な能力の一つと考えています。iOSの開発者として、私たちはUIApplicationからNSFileManagerまで、シングルトンとの相互作用に精通しています。オープンソースプロジェクトやAppleのサンプルコードとStackOverflowの上でシングルトン用法の無数の例を見ている。Xcodeでさえ、デフォルトのコードスニペットに dispatch_once
スニペットがあり、コードにシングルトンを追加することが信じられないほど容易である。
+ (instancetype)sharedInstance
{
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
これらの理由から、シングルトンは、 IOSのプログラミングにおいて一般的です。問題は、不正な使用するのは簡単だということです。
他の人はシングルトンを「アンチパターン、 ''悪'と'病的嘘つき」と呼ぶが、私は完全にシングルトンの良さを排除しない。代わりに、私はシングルトンでいくつかの問題を実証したいです。その結果、次にdispach_once
スニペットを自動補完し,結果を2度考えます。
Global State
ほとんどの開発者は、グローバルで可変な状態が悪いことであることに同意します。ステートフルは、プログラム理解とデバッグを難しくする。我々オブジェクト指向プログラマは、コードのステートフルを最小化するという点で関数型プログラミングから学ぶことがたくさんある。
@implementation SPMath {
NSUInteger _a;
NSUInteger _b;
}
- (NSUInteger)computeSum
{
return _a + _b;
}
上記の簡単な数学ライブラリの実装では、プログラマがcomputeSum
を呼び出す前に、適切な値にインスタンス変数の_a
及び_b
を設定することが期待されます。いくつかの問題がここにあります:
1.computeSum
は、明示的なパラメータとして値を取ることによって_a及び_bの状態に依存しているという事実がありません。インターフェイスを調べることと変数が関数の出力を制御することを理解する代わりにこのコードを読んでいるデベロッパーは依存関係を理解するために実装を調べなければならない。隠された依存関係が悪い。
2.computeSum
を呼び出すための準備における_a
と_b
の変更時これらの変数に依存する他のコードの正確性に影響を与えない変更であることを確認する必要がある。このことは、マルチスレッド環境で特に困難です。
上記の例と以下を比較せよ。
+ (NSUInteger)computeSumOf:(NSUInteger)a plus:(NSUInteger)b
{
return a + b;
}
ここでは、 a
及びb
の依存性が明示されています。我々は、このメソッドを呼び出すためにインスタンスの状態を変異させる必要はありません。そして、我々は、このメソッドを呼び出した結果として永続的な副作用があることを心配する必要はありません。このコードの読者に注意点として、私たちも、この方法には、インスタンスの状態を変更しないことを示すために、クラスメソッドにすることができます。
それでは、この例がシングルトンとどのような関連があるのでしょうか。Miško Heveryの言葉によるとシングルトンはよろしくないグローバルな状態です。シングルトンは、明示的に依存関係を宣言することなく、どこでも使用することができます。_a
及び_b
が明示されている依存関係なしcomputeSum
で使用したと同じように、プログラムの任意のモジュールは、 [ SPMySingleton sharedInstance ]
を呼び出し、シングルトンへのアクセスを得ることができます。これは、シングルトンがプログラムのどこかで任意のコードに影響を与えることができ、相互に影響をし合うという副作用を意味します。
@interface SPSingleton : NSObject
+ (instancetype)sharedInstance;
- (NSUInteger)badMutableState;
- (void)setBadMutableState:(NSUInteger)badMutableState;
@end
@implementation SPConsumerA
- (void)someMethod
{
if ([[SPSingleton sharedInstance] badMutableState]) {
// ...
}
}
@end
@implementation SPConsumerB
- (void)someOtherMethod
{
[[SPSingleton sharedInstance] setBadMutableState:0];
}
@end
上記の例では、 SPConsumerA
とSPConsumerB
は、2つの完全に独立したプログラムモジュールです。まだSPConsumerB
は、シングルトンが提供する共有状態を経てSPConsumerA
の動作に影響することが可能です。Bは両者の関係を明らかにし、 Aへの参照を明示的に与えられている場合にのみ可能であるべきです。ここでシングルトンはグローバルとステートフルな性質に起因し、一見無関係なモジュール間の隠された暗黙的な結合を引き起こします。
より具体的な例を見てみましょう。グローバル可変状態で一つの付加的な問題を見せます。我々は我々のアプリ内のWebビューアを構築したいとしましょう。このWebビューアをサポートするために、我々は単純なURLキャッシュを構築します。
@interface SPURLCache
+ (SPCache *)sharedURLCache;
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
@end
Webビューアに取り組んで、開発者は、いくつかの異なる状況で期待どおりに必ずコードが動作するようにするためにいくつかのユニットテストを書くことを開始します。まず、開発者は、デバイスの接続がないときにWebビューアがエラーを示していることを確認するテストを書き込みます。その後、開発者はWebビューアが、サーバーの障害を適切に処理することを確認するテストを書き込みます。最後に、開発者は戻っWebコンテンツが正しく表示されていることを確認するために、基本的な成功事例のためのテストを書きます。開発者は、すべてのテストを実行し、期待どおりに機能します。素敵です。
数ヵ月後、これらのテストは、それが最初に書き込まれてからWebビューアコードが変更されていない場合でも、失敗し始めます!何が起こったのか?
誰かがテストの順番を変えたことと判明します。成功ケースのテストは、最初に実行されています。他の2つが続きます。エラーケースのテストは現在、予想外に成功しています。何故ならばシングルトンURLキャッシュがテスト全体の応答をキャッシュしているからです。
永続的な状態は、ユニットテストの敵であります。何故ならばユニットテストはすべての他のテストに対して独立していることにより有効となるからです。状態が別のテストから取り残されている場合はテスト実行の順番が突然問題になります。
バグの多いテスト、特にそうなるべきでない時にテストが成功するテストはとても良くないことです。
オブジェクトのライフサイクル
シングルトンの主要な問題は、そのライフサイクルです。プログラムにシングルトンを追加する場合は、どちらか一つと簡単に考える。しかし、私が見てきたiOSコードの多くがその仮定を破壊しうる。
例えば、私たちはアプリユーザが友人のリストを見ることができるアプリを構築しているとします。アプリユーザの友人それぞれプロフィール写真があり、アプリがデバイスにそれらの画像をダウンロードしてキャッシュすることができることをアプリに望みます。便利なdispatch_once
スニペットでSPThumbnailCache
シングルトンを書くことを見出すでしょう。
@interface SPThumbnailCache : NSObject
+ (instancetype)sharedThumbnailCache;
- (void)cacheProfileImage:(NSData *)imageData forUserId:(NSString *)userId;
- (NSData *)cachedProfileImageForUserId:(NSString *)userId;
@end
私たちがユーザーがアプリの内部にアカウントを切り替えることができる機能「ログアウト」実装する時を決める日までアプリを作り続け、世界中でうまくいっているように見える。
ユーザーがサインアウトする時、ディスクに永続化されている状態全てをクリーンアップできることを望む。突然、我々は我々の手に厄介な問題を抱えます:
ユーザー固有の状態は、グローバルなシングルトンに格納されています。ユーザがサインアウトするときディスク上のすべての永続的な状態をクリーンアップできるようにしたい。さもなければ私たちはユーザーのデバイス上の孤立したデータを残し、貴重なディスクスペースを無駄にします。ユーザーがサインアウト時と新しいアカウントでのサインイン時新しいユーザーの新しいSPThumbnailCacheを持てるようにしたい。
ここでの問題は、シングルトンは、定義により、一度作成され永遠に存在するインスタンスと仮定されていることです。あなたは上に概説問題のいくつかの解決策を想像できます。ユーザーがサインアウトするときおそらく我々は、シングルトンのインスタンスを取り壊すことができます:
static SPThumbnailCache *sharedThumbnailCache;
+ (instancetype)sharedThumbnailCache
{
if (!sharedThumbnailCache) {
sharedThumbnailCache = [[self alloc] init];
}
return sharedThumbnailCache;
}
+ (void)tearDown
{
// The SPThumbnailCache will clean up persistent states when deallocated
sharedThumbnailCache = nil;
}
これは目に余るシングルトンパターン誤用である。動作しますか?
我々は確かに、このソリューションを機能させることができるが、コストがあまりにも高すぎます。一つは、我々はスレッドの安全性を保証するdispatch_once
ソリューションのシンプルさと[SPThumbnailCache sharedThumbnailCache]
を呼ぶ全てのコードが同じインスタンスが得ることを失ってしまいました。現在、サムネイルキャッシュを利用したコードのコード実行の順序について非常に注意する必要があります。ユーザーがサインアウトのプロセスにある間キャッシュに画像を保存する過程にあるいくつかのバックグラウンドタスクがあると仮定します。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[SPThumbnailCache sharedThumbnailCache] cacheProfileImage:newImage forUserId:userId];
});
我々は、バックグラウンドタスクが完了するまでtearDown
が実行されないことを確信する必要があります。これは新しい画像データが適切にクリーンアップすることを保証する。あるいはサムネイルキャッシュがシャットダウンされる時バックグラウンドタスクがキャンセルされることを確認する必要がある。さもなければ、新しいサムネイルがいい加減に生成され、古くなったユーザーの状態(newImage)がその中に保存されます。
シングルトンインスタンスの明確な所有者がないので、(すなわち、シングルトンが独自のライフサイクルを管理します)シングルトンを「シャットダウン」することが非常に困難になります。
この時点で、私はあなたが、サムネイルキャッシュがこれまでシングルトンであるべきではありません! と言っていると思います。問題は、オブジェクトのライフサイクルが完全にプロジェクトの開始時に理解されないかもしれないということです具体的な例としては、 DropboxのiOSアプリは今までに署名する1つのユーザーアカウントをサポートしていました。我々は複数のユーザーアカウント(個人用とビジネスアカウントの両方)を同時にサインインすることをサポートしたいと思った日までアプリが数年間、この状態で存在していました。突然すべての「単一のユーザーが一度にサインインする」という前提が崩壊し始めました。アプリケーションのライフサイクルに一致するオブジェクトのライフサイクルを想定して、あなたのコードの拡張を制限します。そして、後で製品の要件が変更された時、その仮定を清算する必要があるかもしれません
ここでの教訓は、シングルトンが唯一のグローバルな状態のために保存されるべきであるということです。そして任意のスコープに関連付けられていないことです。状態がアプリの完全なライフサイクルより短い任意のセッションにスコープされているればシングルトンで管理されるべきではありません。ユーザー固有の状態を管理するシングルトンは、コード臭がして、あなたのオブジェクト図の設計をじっくりと再評価する必要があります。
Avoiding Singletons
シングルトンがスコープの状態のため悪いのであれば、どのように我々はそれらを使用しないようにするのか?
上記の例を再検討してみましょう。我々は、個々のユーザーに特有な状態をキャッシュするサムネイルキャッシュを持っているので、ユーザーオブジェクトを定義してみましょう:
@implementation SPUser
- (instancetype)init
{
if ((self = [super init])) {
_thumbnailCache = [[SPThumbnailCache alloc] init];
// Initialize other user-specific state...
}
return self;
}
@end
私たちは今、認証されたユーザー·セッションをモデル化するオブジェクトを持ちこのオブジェクトにユーザー固有の状態を格納することができます。今、友人のリストを表現するビューコントローラがあるとします。
@interface SPFriendListViewController : UIViewController
- (instancetype)initWithUser:(SPUser *)user;
@end
我々は、明示的にビューコントローラに認証されたユーザオブジェクトを渡すことができます。依存オブジェクトに依存関係を渡すのこの技術は、より正式には依存性注入と呼ばれ、大きなアドバンテージをもつ。
1.サインインしたユーザーが存在する時のみにSPFriendListViewController
が示されるべきというこのインターフェイスを明らかにします。
2.SPFriendListViewControllerは、それが使われていると同じくらいの長さでユーザーオブジェクトへの強い参照を維持できます。たとえば、前述の例を更新することにより、バックグラウンドタスク内のサムネイルキャッシュに画像を保存することができます(下記参照):
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[_user.thumbnailCache cacheProfileImage:newImage forUserId:userId];
});
まだ未処理のこのバックグラウンドタスクでは、さらに相互作用を遮断することなく、最初のインスタンスは取り壊されている間、他の場所でアプリケーションのコードは、作成してまったく新しいSPUserオブジェクトを利用することができます。
もう少し第二の点を実証するために、依存性注入の使用前と使用後のオブジェクト図を視覚化してみましょう
こののSPFriendListViewControllerは、現在のウィンドウのrootViewControllerであると仮定します。シングルトンモデルでは、オブジェクト図はこのようになります:
カスタムイメージビューのリストをもつビューコントローラ自体はsharedThumbnailCacheと相互作用します。ユーザーがログアウトする時、rootViewControllerをクリアしユーザをサインイン画面に戻したいです:
ここでの問題は、SPFriendListViewControllerは、まだ(バックグラウンド操作で)コードを実行している可能性があることです。したがって、まだsharedThumbnailCacheに対する未解決の注目すべき呼び出しがあります。
簡単にするためSPApplicationDelegateがSPUserインスタンスを管理するとします(実際には、おそらく軽いアプリケーションデリゲートを維持するために他のオブジェクトに管理を譲りたいでしょう)。SPFriendListViewControllerがwindowにインストールされている場合、ユーザーへの参照が渡されます。このリファレンスはSPProfileImageView同様にオブジェクト図に示すように注ぎ込むことができます。ユーザーがログアウトすると今、私たちのオブジェクト図は次のようになります。
シングルトンを使用する場合オブジェクト図はかなり似ています。それがどうしたと言うのだ。
問題はスコープです。シングルトンの場合、 sharedThumbnailCacheはプログラムの任意のモジュールに対してまだアクセス可能です。ユーザーはすぐに新しいアカウントにサインインしたとします。そのユーザーはSPThumbnailCacheと相互作用している友人を見たいと思うでしょう。
新しいアカウントにユーザーがサインインした時、古いSPThumbnailCacheの破棄に注意を払うことなく新しいSPThumbnailCacheを作り相互作用することが可能となるべきである。古いビューコントローラと古いサムネイルキャッシュは、オブジェクト管理のごく普通の規則に則り、自発的にバックグラウンドでリラックスしてクリーンアップされるべきです。要するに、私たちは、ユーザBに関連付けられている状態とユーザAに関連付けられている状態を分離する必要があります
#結論
願わくば、この記事には特に小説として読むものは何もないでしょう。人々は何年もの間シングルトンの乱用に対して不満を持っている。そして我々はすべてのグローバル状態が悪いと知っています。しかし、iOSの開発の世界では、シングルトンは、とても一般的なものなので時々、他の場所で、オブジェクト指向プログラミングの年から学んだ教訓を他のどこかで忘れる可能性がある。
このすべてから鍵は、オブジェクト指向プログラミングで我々は可変状態の範囲を最小限にしたいということです。シングルトンはそれとは正反対になります。それはプログラムのどこからでも変更可能な状態にアクセスできるようにするためです。あなたがシングルトンを使用する次回、私はあなたが、代替としての依存性注入を検討すると思います。