🍎

カテゴリ内のメソッドで動的にクラスメソッドを実行する

2020/09/20に公開

同じ動きをするメソッドがあったとき、そのメソッドを持つクラスをサブクラスにしてオーバーライドすれば記述をまとめられますが、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