Objective-Cで"@dynamic"の使いどころを考える

2013年1月5日
objc / ios /

Objective-Cの @dynamic はお好きですか?

ぼくはけっこう好きです。

@synthesizeのほうは昔はほぼ必須で書かないといけなかったり Xcode4.4で省略できるようになった ことで有名ですが、いっぽうで@dynamicのほうはあまり日の目を浴びていない気がします。

そこで、今日は@dynamicについて再考してみることにしました。
以下、ぼくが思い返してみて@dynamicがこんなときに便利だったと感じたところを2点挙げさせていただきます。
みなさまのほうでも「こんなとき便利だよ」というのがありましたら是非ご教示ください。

クラスの内部実装が適当なのを隠すときに

そもそもこの実装自体がどうかという話もあるのですが、リファクタリング前にひとまず雑な実装をしてしまうことはままあります。
例えば、ゲームスコアを管理するGameScoreクラスを作ったとして、その中で ハイスコア とかいくつかのスコアを管理する際、scoresというNSDictionaryインスタンスにささっと入れていたとします(ほんとは普通にプロパティにしたほうが良いですが例として)。

#import "GameScore.h"

@interface GameScore ()
@property (strong) NSMutableDictionary* scores;
@end 

@implementation GameScore

- (id)init
{
  if ((self = [super init])) {
    self.scores = [@{
      @"highScore": @1000,
      @"currentScore": @530,
      @"friendScore": @890,
    } mutableCopy];
  }
  return self;
}

この ハイスコア を外(他のクラス)から参照させたい場合、どう実装しますか?

ダメそうな例 : NSDictionaryのインスタンスをそのまま公開してしまう

@interface GameScore : NSObject
@property (strong) NSMutableDictionary* scores;
@end

手っ取り早いのはscoresプロパティをそのままPublicなプロパティとしてアクセス可能にしてしまう方法でしょうか。
たしかに簡単ですが、外部から内部データ(NSDictionary)を好きなようにいじられたり見られたりするのは避けたいところです。このままだとかなり密結合なかんじです。
Privateな範囲だったらまだ雑でも(後からリファクタリングすれば)良いかもしれませんが、外部から使われる部分はきちんとして疎結合にしておきたいところです。

本当に必要な部分だけ公開する

だったら本当に必要な部分だけ公開すべきということで、こういうのはどうでしょう?

@interface GameScore : NSObject
- (NSUInteger)highScore;
@end
- (NSUInteger)highScore
{
  return [self.scores[@"highScore"] unsignedIntegerValue];
}

highScoreというハイスコアだけを参照するPublicメソッドを追加してみました。
こっちのほうが筋が良さそうです。
これでNSDictionaryで管理している適当な実装を後から修正しても外のクラスには影響しない形になりました。

さらに @dynamic がお好きな人向けに

ただ、ぼくは@dynamicを愛しているのでここに@dynamicを突っ込んでいきます。

@interface GameScore : NSObject
@property (assign, readonly) NSUInteger highScore;
@end
@dynamic highScore;
- (NSUInteger)highScore
{
  return [self.scores[@"highScore"] unsignedIntegerValue];
}

やってることは1個前のサンプルと全く同じですが、@propertyで公開したほうがなんかかっこいい(自己満足)気がします。
強いて言うなら、こっちの場合、ハイスコアを@property (assign) NSUInteger highScore;というPrivateプロパティで管理するようにリファクタリングした後もPublicな@property (assign, readonly) NSUInteger highScore;とそのまま連携できるというメリットはあります(本当に強いて言うならですが)。

重いクラスの初期化を簡便に書きたいときに

例えば、先ほどのGameScoreクラス内でHeavyFunctionというとっても重い(初期化に時間がかかるとかメモリをたくさん消費するとか)クラスを使う必要があったとします。
とっても重いので、

ところです。
ということで、以下のように実装してみました。

普通に実装してみた例

@interface GameScore ()
@property (strong) HeavyFunction* heavyFunction;
@end 

- (void)doSomething1
{
  if (!self.heavyFunction) {
    self.heavyFunction = [HeavyFunction new];
  }
  [self.heavyFunction execute];
}

- (void)doSomething2
{
  if (!self.heavyFunction) {
    self.heavyFunction = [HeavyFunction new];
  }
  [self.heavyFunction execute];
}

まず、HeavyFunctionのインスタンスをPrivateなプロパティとして保持するようにしています。
doSomething1doSomething2HeavyFunctionを利用したメソッドとすると、それが呼ばれたタイミングではじめてHeavyFuncitonnewされるよう留意してあります。具体的には、各メソッド内でself.heavyFunctionnilかどうかをチェックして、nilならHeavyFunctionnewするという実装にしています。
どうやらこれで事足りそうです。

さらに @dynamic がお好きな人向けに

ただ、ぼくは@dynamicを愛しているのでここに@dynamicを突っ込んでいきます。

@interface GameScore ()
@property (strong) HeavyFunction* heavyFunctionInstance;
@property (readonly) HeavyFunction* heavyFunction;
@end 

@dynamic heavyFunction;
- (HeavyFunction*)heavyFunction
{
  if (!self.heavyFunctionInstance) {
    self.heavyFunctionInstance = [HeavyFunction new];
  }
  return self.heavyFunctionInstance;
}

- (void)doSomething1
{
  [self.heavyFunction execute];
}

- (void)doSomething2
{
  [self.heavyFunction execute];
}

まず、heavyFunctionプロパティとは別にheavyFunctionInstanceというプロパティを設け、インスタンスはそのプロパティに持たせるようにしてしまいました。
そしてheavyFunctionプロパティのほうはreadonlyにし、かつ、@dynamicでgetterを自分で書きます。
そのgetterの中でheavyFunctionInstanceプロパティのnilチェック & インスタンス作成をするようにしています。

こうすることで、実際にHeavyFunctionを使うdoSomething1doSomething2の中では、インスタンスが既に存在するかどうかを気にせずにheavyFunctionプロパティを使うだけですむようになりました。
heavyFuncitonを使う箇所が増えれば増えるほどこのメリットは大きくなると思います。

@dynamic は省略できます…

最後になりますが、じつは上記2つのサンプルのどちらでも@dynamicの行が省略可能です。
ぼくは@dynamicを愛していますが、実際のところこの@dynamicを省略してしまっています。
それでもぼくは@dynamicを愛しています。 目に見える愛だけが全てではないということなのだと思います。。。

Related Entries
Latest Entries