A Day In The Life

とあるプログラマの備忘録

メソッド設計で守るべき10個のルール

以前メソッド設計の原則に関する記事を書きましたが

それ以前にメソッド設計する上で最低限守った方がよいルールをまとめてみました。

  1. プロパティをメソッドの戻り値代わりに使ってはいけない
  2. ファンクションメソッドでプロパティの値を変更してはいけない
  3. プロパティをリターンしない
  4. インスタンス変数やプロパティをメソッドの引数に渡さない
  5. 参照渡の引数をリターンしてはいけない
  6. 例外処理を GoTo 文の代わりに使ってはいけない
  7. 理由なく id 型をメソッドの戻り値にしない
  8. 特定メソッドの呼びだしが前提になったメソッドを作ってはいけない
  9. パブリックメソッドからパブリックメソッドを呼ばない
  10. プライベートメソッドからパブリックメソッドを呼ばない

以下その詳細です。

プロパティをメソッドの戻り値代わりに使ってはいけない

メソッドが呼ばれるたびにプロパティを release して作り直すような場合、プロパティを使う必要がありません。プロパティに限らずインスタンス変数を使用する場合も同じです。

悪い例
@interface Hoge : NSObject {
  NSArray *_array;
}
@property (retain) NSArray *array;
- (void)foo;
@end

@implementation Hoge
@synthesize array = _array;
- (void)foo {
  [self.array release];
  : 何らかの処理
  self.array = 処理結果;
}
@end
改善例

メソッドの戻り値ですむならプロパティ使用をやめる。なぜプロパティが必要なのかよく考えてからプロパティを宣言すること。

@interface Hoge : NSObject {
}
- (NSArray *)foo;
@end

@implementation Hoge
- (NSArray *)foo {
  : 何らかの処理
  return 処理結果;
}
@end

ファンクションメソッドでプロパティの値を変更してはいけない

ファンクションメソッド内でプロパティの値を変更してしまうと副作用が発生することになります。メソッド呼び出しが他のメソッドの処理結果に影響を与えてしまうので副作用のあるファンクション設計はやめましょうという話です。
詳細は以下の記事を参考にしてください。

悪い例
@interface Hoge : NSObject {
  NSArray *_array;
}
@property (retain) NSArray *array;
- (NSString *)foo;
@end

@implementation Hoge
@synthesize array = _array;
- (NSString *)foo {
  : 何らかの処理
  self.array = [NSArray arrayWithObjects:@"aaa", @"bbb", nil];
  : 何らかの処理
  return 処理結果;
}
@end
改善例

ファンクションメソッドではプロパティの値を変更しない。呼び出し元で行うこと。
ファンクションメソッドでは常に単純な処理のみを行うようにすること。ファンクションメソッドでプロパティの値を変更してしまうと呼び出し元でその影響が見えなくなることがある。

@interface Hoge : NSObject {
  NSArray *_array;
}
@property (retain) NSArray *array;
- (NSString *)foo;
@end

@implementation Hoge
@synthesize array = _array;
- (NSString *)foo {
  : 何らかの処理
  return 処理結果;
}
@end

呼び出し元

- (void)bar {
  Hoge *hoge = [[Hoge alloc] init];
  NSString *returnValue = [hoge foo];
  hoge.array = [NSArray arrayWithObjects:@"aaa", @"bbb", nil];
  : 後続処理
}

プロパティをリターンしない

メソッドの戻り値にするのであればそもそもプロパティにする必要ないですね。

悪い例
@interface Hoge : NSObject {
  NSArray *_array;
}
@property (retain) NSArray *array;
- (NSArray *)foo;
@end

@implementation Hoge
@synthesize array = _array;
- (NSArray *)foo {
  : 何らかの処理
  self.array = 処理結果;
  return self.array;
}
@end
改善例

プロパティを使わずにメソッドの戻り値を使う。

@interface Hoge : NSObject {
}
- (NSArray *)foo;
@end

@implementation Hoge
- (NSArray *)foo {
  : 何らかの処理
  return 処理結果;
}
@end

インスタンス変数やプロパティをメソッドの引数に渡さない

クラス内でどこからでもアクセスできる変数を引数に渡してもあまり意味がありません。

悪い例
@interface Hoge: NSObject {
  NSString *_title;
}
@property (copy) NSString *title;
- (void)foo:(NSString *)title;
- (void)bar;
@end

@implementation
@synthesize title = _title;
- (void)foo:(NSString *)title {
  NSMutableString *s = [NSMutableString stringWithString:@"Hello "];
  [s appendString:title];
  : 何らかの処理
}
- (void)bar {
  title_ = @"World";
  [self foo:_title];
  // [self foo:self.title];
}
@end
改善例1

引数を廃止する

@interface Hoge: NSObject {
  NSString *_title;
}
@property (copy) NSString *title;
- (void)foo;
- (void)bar;
@end

@implementation
@synthesize title = _title;
- (void)foo {
  NSMutableString *s = [NSMutableString stringWithString:@"Hello "];
  [s appendString:_title];
  : 何らかの処理
}
- (void)bar {
  _title = @"World";
  [self foo];
}
@end
改善例2

インスタンス変数を廃止する

@interface Hoge: NSObject {
}
- (void)foo:(NSString *)title;
- (void)bar;
@end

@implementation
- (void)foo:(NSString *)title {
  NSMutableString *s = [NSMutableString stringWithString:@"Hello "];
  [s appendString:title];
  : 何らかの処理
}
- (void)bar {
  NSString *title = @"World";
  [self foo:title];
}
@end

参照渡の引数をリターンしてはいけない

なんのために参照渡しているか全くわかりませんね。引数をリターンするのは論外です。

悪い例
@interface Hoge : NSObject {
}
- (NSArray *)fooWithArray:(NSArray **)array;
@end

@implementation Hoge
- (NSArray *)fooWithArray:(NSArray **)array {
  : 何らかの処理
  *array = 処理結果;
  return *array;
}
@end

呼び出し元

- (void)bar {
  NSArray *array;
  array = [self fooWithArray:&array];
}
改善例

引数の参照渡をやめる。

@interface Hoge : NSObject {
}
- (NSArray *)foo;
@end

@implementation Hoge
- (NSArray *)foo {
  : 何らかの処理
  return 処理結果;
}
@end

例外処理を GoTo 文の代わりに使ってはいけない

例外処理はジャンプ命令ではありません。

悪い例
- (void)foo {
  @try {
    : 何らかの処理
    if (処理結果不正) {
      @throw [NSException exceptionWithName:@"" reason:@"" userInfo:nil];
    }
    : 何らかの処理
  } @catch (NSException *e) {
    NSLog(@"Error!");
  }
}
改善例

リターンで処理を終了させる。

- (void)foo {
  : 何らかの処理
  if (処理結果不正) {
    NSLog(@"Error!");
    return;
  }
  : 何らかの処理
}

理由なく id 型をメソッドの戻り値にしない

呼び出し側でどんな型のデータが戻ってくるかわからないので明示的に戻り値がわかるように設計しましょう。ただし init 系のメソッド(クラスファクトリメソッドも含む)はのぞきます。

悪い例
- (id)foo {
  
}
改善例

メソッドの戻り値の型は明示的に宣言する。

- (NSArray *)foo {
  
}
補足

Objective-Cは動的型付け言語の特性と静的型付け言語の特性両方を持っています。メソッドの戻り値やプロパティの型など、使う側に仕様をわかりやすく伝える必要がある場面では静的型付け言語のメリットを生かして明示的に型を指定する。逆に変数の型に縛られずに柔軟なプログラミングをしたい場合は動的型付け言語の特性を生かすためにid型を使うといった使いわけが必要です。
id型の使いどころとしては

  • メソッドの戻りをid型で受ける
  • メソッドの引数をid型にする
  • コンテナクラスで管理するデータの型

などがあげられます(常にこうしろというわけではありません)。

// メソッドの戻りをid型で受ける
- (void)bar {
  Hoge *hoge = [[Hoge alloc] init];
  id array = [hoge foo];
  : 後続処理
}
// メソッドの引数をid型にする
- (IBAction)respondToButtonClick:(id)sender {
  : 処理
}

JavaやC♯なんかで

という場面でid型を使うと良いと思います。

特定メソッドの呼びだしが前提になったメソッドを作ってはいけない

fooメソッドと呼ばないと bar メソッドは正しく処理をしませんという場合、呼び出し元で bar メソッドを呼んだのに想定した処理が走らないなんてことがおこります。
あるメソッドを呼ばないと正しく動かないメソッドというのは、そもそも処理をメソッドとして独立させる必要がありません。

悪い例
@interface Hoge : NSObject {
  NSArray *_array;
}
@property (retain) NSArray *array;
- (void)foo;
- (void)bar;
@end

@implementation Hoge
@synthesize array = _array;
- (void)foo {
  : 何らかの処理
  self.array = [NSArray arrayWithObjects:@"aaa", @"bbb", nil];
  : 何らかの処理
}
- (void)bar {
  // arrayプロパティに値がセットされていないと呼び出しても処理が走らない!
  for (NSString * s in self.array) {
    : 何らかの処理
  }
}
@end
改善例

メソッドを統合する。

@interface Hoge : NSObject {
}
- (NSString *)foo;
@end

@implementation Hoge
@synthesize array = _array;
- (void)foo {
  : 何らかの処理
  NSArray *array = [NSArray arrayWithObjects:@"aaa", @"bbb", nil];
  for (NSString * s in array) {
    : 何らかの処理
  }
}
@end

パブリックメソッドからパブリックメソッドを呼ばない

あくまで同一クラス内の話です。メソッドを使う側がインタフェースを見ただけでどのような処理が行われるのかわかりづらくなってしまいます。
特にファンクションメソッドからプロシージャメソッドを呼ぶと副作用が発生する可能性が高いので避けた方がよいと思います。

悪い例
@interface Hoge : NSObject {
}
- (void)foo;
- (void)bar;
@end

@implementation Hoge
@synthesize array = _array;
- (void)foo {
  : 何らかの処理
}
- (void)bar {
  [self foo];
  : 何らかの処理
}
@end

呼び出し元

- (void)methodA {
  Hoge *hoge = [[Hoge alloc] init];
  [hoge foo];
}
- (void)methodB {
  Hoge *hoge = [[Hoge alloc] init];
  [hoge bar];
}
改善例1

どのメソッドを呼ぶべきか呼び出し元で判断させる

@interface Hoge : NSObject {
}
- (void)foo;
- (void)bar;
@end

@implementation Hoge
@synthesize array = _array;
- (void)foo {
  : 何らかの処理
}
- (void)bar {
  : 何らかの処理
}
@end

呼び出し元

- (void)methodA {
  Hoge *hoge = [[Hoge alloc] init];
  [hoge foo];
}
- (void)methodB {
  Hoge *hoge = [[Hoge alloc] init];
  [hoge foo];
  [hoge bar];
}
改善例2

呼び出し順が決まっているのであれば外部公開用の別メソッドを作成してそれを呼び出すようにする。

@interface Hoge : NSObject {
}
/* プライベートメソッドにする
- (void)foo;
- (void)bar;
 */
- (void)newFoo;
- (void)newBar;
@end

@implementation Hoge
@synthesize array = _array;
- (void)foo {
  : 何らかの処理
}
- (void)bar {
  : 何らかの処理
}
- (void)newFoo {
  [self foo];
}
- (void)newBar {
  [self foo];
  [self bar];
}
@end

呼び出し元

- (void)methodA {
  Hoge *hoge = [[Hoge alloc] init];
  [hoge newFoo];
}
- (void)methodB {
  Hoge *hoge = [[Hoge alloc] init];
  [hoge newBar];
}

プライベートメソッドからパブリックメソッドを呼ばない

これも同一クラス内の話です。プライベートメソッドは必ずといっていいほどパブリックメソッドから呼び出されるので、実質的にパブリックメソッドからパブリックメソッドを呼ぶのと同じです。

悪い例
@interface Hoge : NSObject {
}
- (void)foo;
- (void)bar;
@end

@implementation Hoge
@synthesize array = _array;
- (void)baz {
  [self foo];
  : 何らかの処理
}
- (void)foo {
  : 何らかの処理
}
- (void)bar {
  [self baz];
  : 何らかの処理
}
@end
改善例

プライベートメソッドからパブリックメソッドを呼ぶのをやめる。

@interface Hoge : NSObject {
}
- (void)foo;
- (void)bar;
@end

@implementation Hoge
@synthesize array = _array;
- (void)baz {
  : 何らかの処理
}
- (void)foo {
  : 何らかの処理
}
- (void)bar {
  [self baz];
  : 何らかの処理
}
@end

呼び出し元

- (void)methodA {
  Hoge *hoge = [[Hoge alloc] init];
  [hoge foo];
  [hoge bar];
}

まとめ

これらのルールは外部に公開するメソッド(パブリックメソッド)を設計する場合特に重要になります。ソースコードを非公開でインタフェースのみを公開する事を考えた場合、これらのルールに反したメソッドは使う側から見れば何が起こるかわからない危険なメソッドになります。もちろん例外もありますし、あえてルールを無視する場合もあります。設計をするときに頭の片隅にでも置いてもらえれば幸いです。