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

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

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

```objectivec
-(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: だけを持つ専用パラメタクラスを作る。
別ファイルにしない理由は、「そのクラス専用のパラメタクラスである」事が分かるようにしたいため。もしも複数のクラスで同じ専用パラメタクラスを使い回す場合は、それが分かった時に別ファイルに分ける。

```objectivec
@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に書き換えていく。

```objectivec
-(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: forKey`や`objectForKey`している箇所をざっと見て、そのKey毎にプロパティを追加します。プロパティ名にはkeyで使った文字列そのまま使うのが良いです(5.でのtypo防止のため)

```objectivec
@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: forKey`や`objectForKey`している箇所を、プロパティアクセスに変更する。

```objectivec
-(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で渡してるプロパティを、実際に使っているクラスに変更する。

```objectivec
@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](https://qiita-image-store.s3.amazonaws.com/0/7946/365c9d87-90ea-3d80-f6e2-aadd4f287086.png "Refactor_と_Edit_と_Menubar.png")


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

```objectivec
@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](https://qiita-image-store.s3.amazonaws.com/0/7946/2dbd997c-d55f-305a-fa33-d38c0a9f7481.png "Refector.xcodeproj_—_ViewController.m.png")

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

後日談
----
(後で書く）