カテゴリ内のメソッドで動的にクラスメソッドを実行する
同じ動きをするメソッドがあったとき、そのメソッドを持つクラスをサブクラスにしてオーバーライドすれば記述をまとめられますが、Foundation
のクラス(NSString
など)にカテゴリとしてメソッドを追加する場合、そういった考え方を持ち込みにくいことがあります。
@interface NSString (centralClass)
- (NSString *)methodWithString:(NSString *)bar value:(NSUInteger)baz;
@end
…
@implementation NSString (classA)
- (NSString *)fooA
{
return [self methodWithString:classA.bar value:classA.baz];
}
@end
@implementation NSString (classB)
- (NSString *)fooB
{
return [self methodWithString:classB.bar value:classB.baz];
}
@end
@implementation NSString (classC)
- (NSString *)fooC
{
return [self methodWithString:classC.bar value:classC.baz];
}
@end
私の直面した例をそのまま載せられないので分かりにくい具体例ですみませんが、類似した3つのマネージャクラスはどれも+bar
クラスメソッドと+baz
クラスメソッドを備え、クラスごとに定義した文字列定数や整数定数を返します(動的にしていきたいためconst
は使っていません)。
どのメソッドも、+bar
と+baz
の2種類のクラスメソッドを使う点は共通しているので、これをくくれないかと悩んでいました。引数にクラスを与えれば動的にメソッドを変えられそうです。
##プロトコルを用いた実験
まず、上記classA, classB, classCは同じメソッド名で実装している必要があるためプロトコルを準備しました。
@protocol Managing
@required
+ (NSString *)bar;
+ (NSUInteger)baz;
@end
マネージャクラスのプロトコルなので、この例ではManaging
と命名しました。classA, classB, classCはどれも<Managing>
を指定して準拠させます。その後、まとめた実装を管理するcentralClassに以下のように宣言します。
@interface NSString (centralClass)
- (NSString *)methodWithClass:(Class <Managing>)aClass;
@end
ここでうっかり(Class <Managing> *)
としてしまい私はハマりました。Class型はオブジェクトではないので*
は不要です。うっかり*
を付けると普段見なれない警告が並びます。
##Class型引数へのプロトコル指定は無効
メソッド内の具体例が思いつかないので概要だけ書きますが、[aClass bar]
や[aClass baz]
と書くことで、引数に渡ったクラスによって実行時にクラスメソッドが動的に決定されます。
動的にメソッドを使用する場合-respondsToSelector:
の使用がお決まりですが、プロトコルによってメソッドの実装は必須になっているため、これは不要と考えました。しかし、ここで試しにプロトコルに準拠していないclassDを引数に指定してみると、コンパイラから警告が出てきません。そして実行時に例外が投げられました。
どうやらClass型はオブジェクトではないので、(Class <Managing>)
といった引数の型でのプロトコル指定は無意味なようです。
##-respondsToSelector:で実行時に検証する
インタフェース上で引数の意図を目視できるよう(Class <Managing>)
の記述は残したのですが、実装内では-respondsToSelector:
を使わないと事故に繋がります。ですが、ここも引数がClass型という特殊さのため、そのまま書くとエラーが出るようです。
if ([aClass respondsToSelector:@selector(bar)]) { …
こう書くと通りません。指定クラスがクラスメソッドbar
を備えているかどうか検証するためには、次のように書きます。
if ([(Class)aClass respondsToSelector:@selector(bar)]) { …
Class型引数であるaClass
を改めて(Class)
でキャストすると、望んでいる動作になります。これで動的に引数で指定したクラスがクラスメソッドを備えているか検証できるようになりました。
##結論
実験と検証の結果、冒頭の内容は次のようになりました。
@interface NSString (centralClass)
- (NSString *)methodWithClass:(Class <Managing>)aClass;
@end
…
@implementation NSString (classA)
- (NSString *)fooA
{
return [self methodWithClass:classA.class];
}
@end
@implementation NSString (classB)
- (NSString *)fooB
{
return [self methodWithClass:classB.class];
}
@end
@implementation NSString (classC)
- (NSString *)fooC
{
return [self methodWithClass:classC.class];
}
@end
カテゴリのため引数にself.class
と書けないのは少し惜しい気もしますが、記述をシンプルにすることができました。
しかし、そもそもカテゴリなんて使わずにクラスメソッドと引数で実装したほうが美しい気もします。
Discussion