Associative Storage: カテゴリに疑似インスタンス変数を追加
by borealkiss
イントロダクション
Objective-Cのカテゴリはサブクラスを作ることなくメソッドの拡張(またはメソッドごとの分類)が行える点で強力だ。しかしメソッドの定義はできてもインスタンス変数の追加ができない。そこでCocoaのKey Value Coding (KVC)の機能を用いてカテゴリに擬似的にインスタンス変数を生成させる。これにより元クラスを手直しすることなくインスタンス変数を追加することができる。なおこの手法は、よりフレキシブルという理由でNSMapTableを用いるのが一般的なようだが、ここではNSMutableDictionaryを代用した。理由は使い慣れているからだ。NSMapTableを用いた方法はBuck and Yacktman – Cocoa Design Patternsが詳しい。
具体例
簡単な例としてボタンを押すとアラートを表示するアプリケーションを考える。ボタンプッシュ後の動作はAppControllerクラスで以下のように定義されているとする。
//AppController.m @implementation AppController -(IBAction)buttonPressed:(id)sender{ NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil); } @end
ここで「一度アラート表示された場合は再度アラートを表示させない」仕様に変更するとしよう。AppControllerにBOOL値を格納するインスタンス変数を追加して制御するのが手っ取り早いが、ここではAppControllerのカテゴリ(ShowAtOnce)を新規に定義して制御することにする。カテゴリ作成の大まかな手順は以下のようになる。
- 疑似インスタンス変数格納用にNSMutableDictionary変数をカテゴリに定義
- 疑似インスタンス変数としたいオブジェクトを上記NSMutableDictionary変数に格納。その際のキーストリングはAppControllerインスタンスに固有のものを用いる
- 格納したオブジェクトのAccesors (Setters/Getters)をカテゴリに定義
- インスタンスの解放時にNSMutableDictionaryからオブジェクトを消去するメソッドをカテゴリに定義
以下順にみていこう。
1. 疑似インスタンス変数格納用変数の定義
疑似インスタンス変数にしても情報を格納する場所は必要になる。そこで疑似インスタンス変数を格納する場所としてNSMutableDictionaryオブジェクトをカテゴリ(ShowAtOnce)に定義する。さらにこのオブジェクトのAccesorsをクラスメソッドとして定義する。つまりこの格納用のNSMutableDictionaryオブジェクトはAppControllerインスタンスすべてで使い回されることになる。
//AppController-ShowAtOnce.m @implementation AppController (ShowAtOnce) static NSMutableDictionary *_simulatedIVars = nil; +(NSMutableDictionary *)_simulatedIVars{ if (_simulatedIVars == nil){ _simulatedIVars = [[NSMutableDictionary alloc] init]; } return _simulatedIVars; } @end
2. インスタンス固有のキーストリング
上記例のアプリケーションの仕様を実現するためには、一度アラートが表示されたかどうかを調べるBOOL値を(NSMutableDictionary *)_simulatedIVarsに格納できればよさそうだ。しかし今は疑似インスタンス変数を作りたいのでAppControllerインスタンスごとに異なる値を格納できるようにする必要がある。そこでまず「AppControllerインスタンスに固有なNSStringオブジェクト」を生成するメソッドを定義する。このメソッドは-[AppController self]のストリング表現を返す。
//AppController-ShowAtOnce.m -(NSString *)_instanceID{ return [NSString stringWithFormat:@"%@", self]; }
さらに「疑似インスタンス変数名に相当するNSStringオブジェクト」を用意する。これはクラス全体で使い回しができるようにすればよい。ここではAlertHasEverBeenShownKeyStringという名前で定義しておこう。
//AppController-ShowAtOnce.m static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown";
あとは「AppControllerインスタンスに固有なNSStringオブジェクト」と「疑似インスタンス変数名に相当するNSStringオブジェクト」を適当に繋げて「あるAppControllerインスタンスの疑似インスタンス変数に固有なNSStringオブジェクト」を作成するメソッドを定義する。このメソッドの返り値が(NSMutableDictionary *)_simulatedIVarsへの格納用キーストリングとして利用されるわけだ。
//AppController-ShowAtOnce.m -(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{ return [NSString stringWithFormat:@"%@:%@", [self _instanceID], aKeyString]; }
現在までのAppController (ShowAtOnce)の中身をまとめると以下のようになっているはずだ。
//AppController-ShowAtOnce.m static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown"; @implementation AppController (ShowAtOnce) static NSMutableDictionary *_simulatedIVars = nil; +(NSMutableDictionary *)_simulatedIVars{ if (_simulatedIVars == nil){ _simulatedIVars = [[NSMutableDictionary alloc] init]; } return _simulatedIVars; } #pragma mark - #pragma mark Utilities -(NSString *)_instanceID{ return [NSString stringWithFormat:@"%@", self]; } -(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{ return [NSString stringWithFormat:@"%@:%@", [self _instanceID], aKeyString]; } @end
3. 疑似インスタンス変数のAccesors
上記メソッド群を利用してカテゴリ(ShowAtOnce)に定義する。Settersは-[AppController _setAlertHasEverBeenShown:]、Gettersは-[AppController _alertHasEverBeenShown]としよう。ここはstraightforwardだと思う。
//AppController-ShowAtOnce.m -(BOOL)_alertHasEverBeenShown{ NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; id object = [[[self class] _simulatedIVars] objectForKey:aKey]; if (object == nil){ return NO; } return [object boolValue]; } -(void)_setAlertHasEverBeenShown:(BOOL)yn{ NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; [[[self class] _simulatedIVars] setObject:[NSNumber numberWithBool:yn] forKey:aKey]; }
あとはAppController内でこのメソッドを使ってやればよい。具体的には以下のようになるだろう。
//AppController.m #import "AppController-ShowAtOnce.h" @implementation AppController -(IBAction)buttonPressed:(id)sender{ if (![self _alertHasEverBeenShown]){ NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil); [self _setAlertHasEverBeenShown:YES]; } } @end
4. 疑似インスタンス変数を解放
AppControllerインスタンスごとに(NSMutableDictionary *)_simulatedIVarsに登録されるのだからAppControllerインスタンスが不要になったら疑似インスタンス変数も解放する必要がある。そのために以下のようなメソッドをAppController(ShowAtOnce)に定義しておく。このメソッドは呼ばれれば不要になった疑似インスタンス変数を解放する。
//AppController-ShowAtOnce.m -(void)_removeSimulatedIVars{ NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; id object = [[[self class] _simulatedIVars] objectForKey:aKey]; if (object){ [[[self class] _simulatedIVars] removeObjectForKey:aKey]; } }
さてこれまでAppControllerそのものを一切変更しないように実装すると言ってきたが実は嘘で、AppControllerそのものを手直しする必要がでてくる。というのも疑似インスタンス変数の解放を呼ぶメソッドをカテゴリに書くことは危険だからだ。例えば-[AppController dealloc]をカテゴリ(ShowAtOnce)に再定義することは可能だが、もし他に疑似インスタンス変数を持つ別カテゴリが同様の-[AppController dealloc]を実装した場合どちらの-[AppController dealloc]が呼び出されるかわからなくなるからだ(結果どちらかのカテゴリの疑似インスタンス変数は解放メソッドを呼ばれないがために生き残ってしまう)。したがって疑似インスタンス変数の解放メソッドを呼び出すのはAppControllerとするのがよいだろう。
//AppController.m -(void)dealloc{ [self _removeSimulatedIVars]; [super dealloc]; }
全ソースコード
最終的なコードは以下のようになっている。
//AppController.h #import <Cocoa/Cocoa.h> @interface AppController : NSObject { } -(IBAction)buttonPressed:(id)sender; @end
//AppController.m #import "AppController.h" #import "AppController-ShowAtOnce.h" @implementation AppController -(IBAction)buttonPressed:(id)sender{ if (![self _alertHasEverBeenShown]){ NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil); [self _setAlertHasEverBeenShown:YES]; } } -(void)dealloc{ [self _removeSimulatedIVars]; [super dealloc]; } @end
//AppController-ShowAtOnce.h #import <Cocoa/Cocoa.h> #import "AppController.h" @interface AppController (ShowAtOnce) +(NSMutableDictionary *)_simulatedIVars; -(void)_removeSimulatedIVars; -(BOOL)_alertHasEverBeenShown; -(void)_setAlertHasEverBeenShown:(BOOL)yn; -(NSString *)_instanceID; -(NSString *)_objectKeyForKeyString:(NSString *)aKeyString; @end
//AppController-ShowAtOnce.m #import "AppController-ShowAtOnce.h" //Private static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown"; @implementation AppController (ShowAtOnce) static NSMutableDictionary *_simulatedIVars = nil; +(NSMutableDictionary *)_simulatedIVars{ if (_simulatedIVars == nil){ _simulatedIVars = [[NSMutableDictionary alloc] init]; } return _simulatedIVars; } -(void)_removeSimulatedIVars{ NSLog(@"%s", __FUNCTION__); NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; id object = [[[self class] _simulatedIVars] objectForKey:aKey]; if (object){ [[[self class] _simulatedIVars] removeObjectForKey:aKey]; } } #pragma mark - #pragma mark Setters Getters -(BOOL)_alertHasEverBeenShown{ NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; id object = [[[self class] _simulatedIVars] objectForKey:aKey]; if (object == nil){ return NO; } return [object boolValue]; } -(void)_setAlertHasEverBeenShown:(BOOL)yn{ NSString *aKey = [self _objectKeyForKeyString:AlertHasEverBeenShownKeyString]; [[[self class] _simulatedIVars] setObject:[NSNumber numberWithBool:yn] forKey:aKey]; } #pragma mark - #pragma mark Utilities -(NSString *)_instanceID{ return [NSString stringWithFormat:@"%@", self]; } -(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{ return [NSString stringWithFormat:@"%@:%@", [self _instanceID], aKeyString]; } @end