やはりお前らのCoreDataの使い方も間違っている

クックパッドさんが月イチで行っている第6回potatotips(ポテトチップス)という開発Tips共有会が開かれたので参加させてもらい、『やはりお前らのCoreDataの使い方も間違っている』というタイトルで発表させてもらいました。

今回が3回目の発表&参加だったので、過去自分が発表したものもリンクしておきます。

今回の開催場所はUIEvolution株式会社さんということで、あまりメディアで公開されてない社内を少しだけ体験することも出来ました。

本題、お前らがCoreDataを使う上で何を間違っているか

自分が発表した内容はCoreDataに関する次の3つのものです。

  • AppDelegateでNSManagedObjectContextを生成している
  • CoreDataでのデータの取得は全てNSFetchedResultsControllerを必要があるわけではない
  • NSManagedObjectのサブクラスを自動生成しているのに、データのアクセスにsetValue:forKeyを使ってしまっている

順に説明していきます

AppDelegateでNSManagedObjectContext生成して保持しないこと

AppleのXcodeテンプレートから生成されるCoreDataのコードでは、AppDelegateからNSManagedObjectContextを生成して保持してしまっていますが、AppDelegateの役割はiOSから受け取る処理を行うものであって、データの永続化のためにNSManagedObjectContextを生成したりするのは役割として間違っています。

これをテンプレートのまま実際のプロジェクトで使っていくと、確実にAppDelegateが肥大化していくのは言うまでもないですね
( )゚Д゚( ;)

具体的にはXcodeの「Use Core Data」にチェックボックスを入れた場合に生成されるテンプレートは次のようなコードです。

#pragma mark - Core Data stack

// Returns the managed object context for the application.
// If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _managedObjectContext;
}

// Returns the managed object model for the application.
// If the model doesn't already exist, it is created from the application's model.
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel != nil) {
        return _managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CoreDataXcodeTemplate" withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}

// Returns the persistent store coordinator for the application.
// If the coordinator doesn't already exist, it is created and the application's store added to it.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataXcodeTemplate.sqlite"];

    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        /*
         Replace this implementation with code to handle the error appropriately.

         abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 

         Typical reasons for an error here include:
         * The persistent store is not accessible;
         * The schema for the persistent store is incompatible with current managed object model.
         Check the error message to determine what the actual problem was.


         If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.

         If you encounter schema incompatibility errors during development, you can reduce their frequency by:
         * Simply deleting the existing store:
         [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]

         * Performing automatic lightweight migration by passing the following dictionary as the options parameter:
         @{NSMigratePersistentStoresAutomaticallyOption:@YES, NSInferMappingModelAutomaticallyOption:@YES}

         Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.

         */
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }    

    return _persistentStoreCoordinator;
}

これをAppDelegateがやるべきことなの?ってことです。

CoreDataでのデータの取得は全てNSFetchedResultsControllerを必要があるわけではない

UITableViewを使い、保存されている1つのデータをそのまま1つのcellに表示するような使い方ならNSFetchedResultsControllerはとても適していると思いますし、グルーピングを行うのであればsection毎にデータを分けられるのでindexPathでそのままデータにアクセスも出来ます。また、section用の属性は動的に作成もできるので便利です。

参考:NSFetchedResultsControllerで日付毎にセクションをまとめる方法
http://qiita.com/takayama/items/aa3a7acd2f40ffb93dfb

ただ、全てのデータ取得にNSFetchedResultsControllerを使う必要はなくて、例えば取得したいデータが特定のひとつなどであればNSFetchRequestで充分だったりします。

NSManagedObjectのサブクラスを自動生成しているのに、データのアクセスにsetValue:forKeyを使ってしまっている

NSManagedObjectのサブクラスを自動生成しているのであれば、そのサブクラスのプロパティを使えばデータのセットはドット構文でアクセスするだけなのに、わざわざNSMangedObject#setValue:<#オブジェクト#>forKey:<#キー名#>を使ってKeyValue形式でデータにアクセスしてしまっている例もよく見かけます。

これもXcodeが生成するテンプレートが正しいと思ってしまうせいではないかと思います。

まずはキー値でクラスを作ってアクセスしている例

//Eventというクラスを文字列指定で編集しようとする
NSEntityDescription *entity
  = [NSEntityDescription entityForName:@"Event" 
                inManagedObjectContext:managedObjectContext];


//NSManagedObjectをそのまま使っているので
//setValue:forKeyによりキー値コーディングすることになる
[entity setValue:@(1) forKey:@“num"];
[entity setValue:@"hoge" forKey:@“name"];

次に自動生成したクラスからドット構文でアクセスしている例

//クラス名を文字列で取得して名前で呼び出せるように準備
//EXMSubClassは自動生成したNSManagedObjectのサブクラス
NSString *className = NSStringFromClass([EXMSubClass class]);

EXMSubClass *entity
  = [NSEntityDescription entityForName:className 
                inManagedObjectContext:managedObjectContext];


//サブクラスにnumの属性があればプロパティでアクセスできる
entity.num = @(1);
entity.name = @"hoge";

Xcodeが生成しているテンプレートのコードではNSManagedObjectのサブクラスを自動生成していないので、前者のようにせざるを得ないのであって、自動生成していればこんな抽象化はほとんど意味をなさないですしリファクタリングする際にも効率が落ちます。

まとめ

Xcodeのコードテンプレートはそれ自体がベストプラクティスなわけではないし、たいていの場合によってはとても余分なやり方になりますのでテンプレートからプロジェクトを作っていくのは辞めたほうがいいです。

また、私が発表させてもらった内容もプロダクトの性質やプロジェクトメンバーの経験に依存したり、期間に依存する話だったりとケースバイケースであり、決められた開発期間内でまずは動くプロダクトを作ることというものが絶対的に優先順位が高いということは言うまでもありません。しかし、もし、もっといい方法があるとか、いや実はテンプレート通りに作ることのメリットが大きいんだよという事があればコメントを書いてもらうのも良いですが、yimajoが変なことを言ってるのを止めるためにpotatotipsに参加してみるかというきっかけになるのかもしれません。

おわりに

uie.jpg

(写真は順番待ちしてるときにクックパッド社のgfxさんを撮ったもの)

写真で伝えることは出来ませんが、UIEvolution社は組み込み機器での開発も行っているようで、執務室の中には所かしこにカーナビなどの機器があり、ソフトウェア専業の会社にはない雰囲気がありました。私が新卒で入った会社は半導体メーカーだったので、そういうのがとても懐かしく思えました。

次のpotatotips#6はDeNA社が開催場所になるそうです。このような勉強家に参加していて気づいた事としては、こういう会を開催場所としてもらえる会社がエンジニア募集をする場合は業務内容だけではなく現在の課題を話してくれると参加者もツッコめるのではないかなと思います。表面上の業務内容にもし興味がなくても課題を解決できなくないかな、という面からもっと話を聞きたくなりますし、自分が働くときのイメージもできるのではないかと思います。

発表会後、iOSアプリエンジニアだったら誰でも知っている「iPhoneプログラミングUIKit詳解リファレンス」という本を書かれた@tokoromさんにサインを頂きました。

uikit.png



UIKitについて書かれたかなり詳しい本で、iPhoneアプリ開発の現場では机の上に最もよく見かけた本です。リファレンスとしてあの部品の使い方ってどうやるんだろうという時に役に立ちました。

写真

コメントを残す

CAPTCHA