Help us understand the problem. What is going on with this article?

(模索中)リファクタリング:パラメタの受け渡し用NSDictionaryを専用クラス化する方法

More than 5 years have passed since last update.

お願い もっと良い方法があるよ!こうすると楽だよ!ここ間違ってるよ!等があればコメントください。やり方を模索中です

ある日ソースコードを見てると、こんなソースを見つけました。

-(void)methodA
{
    NSDictionary *userInfo;
    NSURL *imageUrl;
    NSString *appid;
    NSMutableDictionary *param = [NSMutableDictionary dictionary];
    [param setObject:userInfo forKey:@"userInfo"];
    [param setObject:imageUrl forKey:@"imageUrl"];
    [param setObject:appid forKey:@"appid"];
    [self methodBWithParam:param];
}
-(void)methodBWithParam:(NSDictionary *)param
{
    // ここで別クラスのメソッドにparamを渡してる
}

methodBWithParamからは別クラスのメソッドを呼び、さらにその先は別クラスのメソッドを呼び、、、と、NSDictionaryをパラメータの集合として引きずり回しています。

結構な量のソースが↑の状態になっていて、これを書いてた本人も「なんとかしたい」と思っているので、専用クラスのプロパティ化する方法を考えました。

注意:まだ「こんな風にしたら間違えにくくて楽かなー」と考えをまとめているだけで、実際にはまだやっていません。実際にやった結果は後日追記したいと思います。

1. paramのたどり着く先を探す

メソッドをどんどん追いかけ、paramが行き着く先のクラス/メソッドを探す。

2. setObject:forKey: とobjectForKey: だけを持つ専用パラメタクラスを作る

1.で探し出したクラスの .h と .m の中に、 setObject:forKey: とobjectForKey: だけを持つ専用パラメタクラスを作る。
別ファイルにしない理由は、「そのクラス専用のパラメタクラスである」事が分かるようにしたいため。もしも複数のクラスで同じ専用パラメタクラスを使い回す場合は、それが分かった時に別ファイルに分ける。

@interface MyParamClass : NSObject
-(void)setObject:(id)obj forKey:(id)key;
-(id)objectForKey:(id)key;
@end

@implementation MyParamClass{
    NSMutableDictionary *_dic;
}
-(id)init
{
    self = [super init];
    if( self ){
        _dic = [NSMutableDictionary dictionary];
    }
    return self;
}
-(void)setObject:(id)obj forKey:(id)key
{
    [_dic setObject:obj forKey:key];
}
-(id)objectForKey:(id)key
{
    return [_dic objectForKey:key];
}
@end

3. paramの型を専用パラメタクラスに書き換える

paramのたどり着く先のメソッドの引数の、(NSDictionary *)だった部分を(MyParamClass*)に書き換える。
すると他のクラスも、型が合わないというエラーを出し始めるので、エラーになっている箇所の型をMyParamClassに書き換えていく。

-(void)methodA
{
    NSDictionary *userInfo;
    NSURL *imageUrl;
    NSString *appid;
    MyParamClass *param = [[MyParamClass alloc]init]; // ←NSDictionaryだったのをMyParamClass alloc/initに書き換え
    [param setObject:userInfo forKey:@"userInfo"];
    [param setObject:imageUrl forKey:@"imageUrl"];
    [param setObject:appid forKey:@"appid"];
    [self methodBWithParam:param];
}
//
// ...
// ↓別クラスのメソッドのつもり
-(void)methodBWithParam:(MyParamClass *)param // ←NSDictionaryだったのをMyParamClassに書き換え
{
    NSDictionary *userInfo = [param objectForKey:@"userInfo"];
    NSURL *url = [param objectForKey:@"imageUrl"];
    NSString *appid = [param objectForKey:@"appid"];

}

もしもsetObject:forKey:objectForKey以外のメソッド(たとえばvalueForKeyとか)が使われていれば、この段階でエラーが残るはず。そうしたら一度立ち止まって考える。この方法では上手く出来ないかもしれない。

4. 専用パラメタクラスにパラメタ毎のプロパティを追加

setObject: forKeyobjectForKeyしている箇所をざっと見て、そのKey毎にプロパティを追加します。プロパティ名にはkeyで使った文字列そのまま使うのが良いです(5.でのtypo防止のため)

@interface MyParamClass : NSObject
-(void)setObject:(id)obj forKey:(id)key;
-(id)objectForKey:(id)key;
@property(nonatomic,strong)id userInfo; // ←追加 ここのidは後で直す。
@property(nonatomic,strong)id imageUrl; // ←追加
@property(nonatomic,strong)id appid;    // ←追加
@end

key部分をマクロで統一している場合は、まずはそのマクロ名を「key名」と見なしてプロパティを追加する。一時的におかしなプロパティ名になるけど、後でプロパティ名の一括変換で対応する。
key部分が「マクロ」と「文字列」で混ざっているなら、マクロを文字列に置き換える。

注意:ここで「keyの名前がおかしい、変えたい」と思ってもぐっと我慢する事 後の作業でtypoしやすくなるから。

5. 専用パラメタクラスを呼び出している箇所を、プロパティアクセスに置き換える

setObject: forKeyobjectForKeyしている箇所を、プロパティアクセスに変更する。

-(void)methodA
{
    NSDictionary *userInfo;
    NSURL *imageUrl;
    NSString *appid;
    MyParamClass *param = [[MyParamClass alloc]init];
//    [param setObject:userInfo forKey:@"userInfo"];
//    [param setObject:imageUrl forKey:@"imageUrl"];
//    [param setObject:appid forKey:@"appid"];
    param.userInfo = userInfo; // ←プロパティアクセスに変更
    param.imageUrl = imageUrl; // ←プロパティアクセスに変更
    param.appid = appid;       // ←プロパティアクセスに変更
    [self methodBWithParam:param];
}
//
// ...
// ↓別クラスのメソッドのつもり
-(void)methodBWithParam:(MyParamClass *)param
{
//    NSDictionary *userInfo = [param objectForKey:@"userInfo"];
//    NSURL *url = [param objectForKey:@"imageUrl"];
//    NSString *appid = [param objectForKey:@"appid"];
    NSDictionary *userInfo = param.userInfo; // ←プロパティアクセスに変更
    NSURL *url = param.imageUrl;             // ←プロパティアクセスに変更
    NSString *appid = param.appid;           // ←プロパティアクセスに変更

}

6. 専用パラメタクラスのsetObject:forKey: とobjectForKey: をコメントアウトしてビルド

プロパティアクセスに変更する箇所が抜け漏れしてると、ここでエラーになるはず。エラーになったら4.と5.を繰り返す。

7. プロパティのid指定を、実際に使っているクラスに変更

idで渡してるプロパティを、実際に使っているクラスに変更する。

@interface MyParamClass : NSObject
// -(void)setObject:(id)obj forKey:(id)key;
// -(id)objectForKey:(id)key;
@property(nonatomic,strong)NSDictionary *userInfo;
@property(nonatomic,strong)NSURL *imageUrl;
@property(nonatomic,strong)NSString *appid;
@end

8. プロパティ名がおかしければXCodeのRefactorで置き換える

プロパティ名を変えたい、マクロ名そのままになってておかしい等あれば、Edit→Refactor→Renameでプロパティ名を変更する。 手作業ではやらない。

Refactor_と_Edit_と_Menubar.png

9. 6.のコメントを戻して、NSAssertを仕込む

これは、複数人で開発している場合に、「他の人の作業が止まらないように」するための仕込み。一人で開発/関係者全員が一気に対応できるなら不要。
XCode5から、QuickLookで見られるコメント表記が可能になったので、これにも説明をいれておきます。また、NS_DEPRECATED_IOSを仕込んで、コードコンプリートで目立つようにしておきます。

@interface MyParamClass : NSObject
// このコメントは、XCode5から使えるフォーマット。QuickLookで↓が表示される。
/*! NSDictionaryで使ってたsetObjectは、専用パラメタクラス化で使えなくなりました。
 * プロパティを使ってください。呼び出すとNSAssert()で止まります。
 * \param obj パラメタの内容
 * \param key パラメタのキー
 */
-(void)setObject:(id)obj forKey:(id)key NS_DEPRECATED_IOS(3_0,3_0); // ←使えなくなった旨をコンパイル時に出す。

/*! NSDictionaryで使ってたobjectForKeyは、専用パラメタクラス化で使えなくなりました。プロパティを使ってください。呼び出すとNSAssert()で止まります。
 * \param key パラメタのキー
 */
-(id)objectForKey:(id)key NS_DEPRECATED_IOS(3_0,3_0); // ←使えなくなった旨をコンパイル時に出す。

@property(nonatomic,strong)NSDictionary *userInfo;
@property(nonatomic,strong)NSURL *imageUrl;
@property(nonatomic,strong)NSString *appid;
@end

@implementation MyParamClass{
    NSMutableDictionary *_dic;
}
-(id)init
{
    self = [super init];
    if( self ){
        _dic = [NSMutableDictionary dictionary];
    }
    return self;
}
-(void)setObject:(id)obj forKey:(id)key
{
    NSAssert(NO,@"setObject:forKey: is not use. Use properties. key=%@ object=%@",key,obj ); // NSAssert追加
    [_dic setObject:obj forKey:key];
}
-(id)objectForKey:(id)key
{
    NSAssert(NO,@"objectForKey: is not use. Use properties. key=%@",key); // NSAssert追加
    return [_dic objectForKey:key];
}
@end

このようにコメントを書く事で、XCode5だと↓のように見えます。

Refector.xcodeproj_—_ViewController.m.png

10.後始末

  • この変更がチームに行き渡ったら、プロパティ以外の部分(init,setObject,objectForKey,_dic)を全部削除する。
  • せっかくなので、各プロパティにコメントで「何のパラメタなのか」の説明を書いておく。
  • ここまでだと、プロパティの値がいつでも変更可能になってしまう。コンビニエンスコンストラクタで必要なパラメタを受け取り、プロパティはreadonlyにしておくと良いかも。

後日談

(後で書く)

paming
Android修行中
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away