Instantly share code, notes, and snippets.

Embed
What would you like to do?

公開用のObjective-Cの書き方

1人で書いているアプリのコードは好きなように書けばいいと思うけど、他人も利用するコードではそうもいかない。 何に気をつければ他人にとって利用しやすいプログラムになるのか考えた話。

ひとことで言うと"誰でも簡単に導入でき、正しく扱うことが出来る"ということが求められる。 もう少し具体的に書くと以下の3つが大切そう。

  • シンボルやリソースが衝突しない
  • 既存のコードへ副作用を与えない
  • 想定外の動作をさせるようなインターフェースを与えない

公開する目的

  • DRY: Don't Repeat Yourself (画像ローダーやLRUキャッシュ機構を毎回書くのは大変)
  • 複数人で不具合修正/機能追加ができ、全員がその恩恵を受けられる。 (これが悪い方向に働くこともある)

アプリとの衝突

利用側のアプリに迷惑をかけないようにする

シンボルの衝突

"duplicate symbols for architecture armv7"の画像を貼ったらわかってもらえそう。 シンボルの衝突の原因は大体以下の2つ。

  • アプリとライブラリのクラス名, メソッド名, 定数名が衝突
  • アプリとライブラリで同一のライブラリに依存し、重複してビルド対象に含まれている

前者はAppleが提示している命名規則を守ることである程度回避できる。(完全ではない。) 後者は重複したライブラリをビルド対象から外すことで回避できるが、CocoaPodsを利用するとより簡単に回避できる。

命名規則による衝突の回避

クラス名

クラス名には3文字の大文字prefixをつける(例: ISHObject)。

独自のクラスには3文字のプレフィックスをつけてください。 会社名やアプリケーション名にちなんだものでも、重要なコンポーネントに由来するものでも構いません。 たとえば、会社名がWhispering Oak、開発するゲームの名前がZebra Surpriseであれば、 WZSやWOZといったプレフィックスにするとよいかもしれません。

(Objective-Cによるプログラミング, p.127)

2文字のプレフィックスは、フレームワーククラスに使うため、Appleが予約しています。

(Objective-Cによるプログラミング, p.126)

定数名

外から見える必要がある定数の場合は関連するクラス名を冠する定数名をつけ、ヘッダーで宣言して実装ファイルで定義する。

extern NSString *const ISHGlobalConstant;
NSString *const ISHObjectGlobalConstant = @"ISHObjectGlobalConstant";

外から見える必要がない場合には実装ファイルで定義し、外から見えないようにする。

static NSString *const ISHLocalConstant = @"ISHLocalConstant";
カテゴリのメソッド名

カテゴリのメソッドには3文字の小文字prefix+アンダースコアをつける(例: ish_method)。

クラス名のプレフィックスと同じ3文字の文字列を、メソッド名の 規約に従って小文字にし、アンダースコアをはさんでメソッド名と連結してください。

(Objective-Cによるプログラミング, p.74)

重複したライブラリの問題

昔よくあったこと: JSONKitのシンボルがぶつかる!

ライブラリが他のライブラリに依存することは悪いことではないが、利用側で衝突が起きないように留意するべき。

気をつけたほうが良いもの
  • 無用な依存は避ける
  • 外部のライブラリに依存していることを明記する
  • CocoaPodsを利用する(重複を自動的に解決してくれる)
守るべきこと
  • static libraryとして配布する場合に依存ライブラリを含まない

リソースの衝突の回避

ライブラリが使用するリソースはNSBundleを分けよう

NSBundleを分けない場合に起こりうること

  • 同名のファイルがアプリに存在した場合、正しいリソースを読み込まなくなる
  • *.lprojが衝突するとNSLocalizedStringが正しく動作しなくなる
#define NSLocalizedString(key, comment) \
	    [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]

ライブラリ用のNSBundleつくりかた

  • LibraryName.bundleっていうディレクトリをつくり、その中にリソースを入れておく。
  • *.lprojもその中に入れてやりNSLocalizedStringに相当するマクロを定義する。
#define ISHLibraryLocalizedString(key, comment) \
	    面倒なので後で書く

良い@interface

  • わかりやすい
  • 想定外の動作をさせるようなインターフェースを与えない

プロパティ

基本方針: 変更の可否を明確にしつつ、なるべく情報を与える。

readwriteなプロパティは変更に適切に対応する

readwriteなプロパティはあらゆるタイミングでの変更が考慮されることが求められる。 例えば、以下のようなキャッシュオブジェクトでは、currentSizeよりも小さい値がsizeLimitにセットされた場合、 removeAllObjectsが呼ばれることが望ましいので、setSizeLimit:はそのように実装されるべき。

実際、NSCacheもそのように実装されている。 https://github.com/ishkawa/sandbox/blob/master/NSCacheBehavior/NSCacheBehaviorTests/NSCacheBehaviorTests.m

@interface ISHCache : NSObject

@property (nonatomic, readonly) NSUInteger currentSize;
@property (nonatomic) NSUInteger sizeLimit;

- (id)objectForKey:(id)key;
- (void)setObject:(id)object forKey:(id)key;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;

@end
@implementation ISHCache

- (void)setSizeLimit:(NSUInteger)sizeLimit
{
    _sizeLimit = sizeLimit;

    if (self.currentSize > sizeLimit) {
        [self removeAllObjects];
    }
}

...

@end

変更に対応しないプロパティはreadonlyにする

さっきの例ではsizeLimitはreadwriteがふさわしいプロパティだったが、currentSizeのようなプロパティはreadonlyがふさわしい。 仮にcurrentSizeがreadwriteだったとしたら、"currentSizeが指定した値になるようにオブジェクトを削除するのかもしれない"といった誤解を招きかねない。 利用者に誤解を与えないためにも、変更に対応していないプロパティはreadonlyにするべき。

仮に例でのsizeLimitを変更に対応させない場合もreadonlyにするべきで、代わりにinitメソッドの引数で与えるのがふさわしい。

@interface ISHCache : NSObject

@property (nonatomic, readonly) NSUInteger currentSize;
@property (nonatomic, readonly) NSUInteger sizeLimit;

- (instancetype)initWithSizeLimit:(NSUInteger)sizeLimit;

- (id)objectForKey:(id)key;
- (void)setObject:(id)object forKey:(id)key;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;

@end
@implementation ISHCache

- (instancetype)initWithSizeLimit:(NSUInteger)sizeLimit
{
    self = [super init];
    if (self) {
        _sizeLimit = sizeLimit;
    }
    return self;
}

...

@end

隠蔽するべきか

判断基準は"そのプロパティを利用者が知る必要があるか"。 ISHCacheの例で、内部的にNSMutableDictionaryをもっているとしたら、それは利用者が知る必要はないプロパティなので隠蔽する。 隠蔽するプロパティはclass extensionで定義すると良い。

@interface ISHCache ()

@property (nonatomic, readonly) NSMutableDictionary internalDictionary;

@end

@implementation ISHCache

- (id)init
{
    self = [super init];
    if (self) {
        _internalDictionary = [NSMutableDictionary dictionary];
    }
    return self;
}

...

@end

言うまでもないが、変更されたくないからといってなんでもclass extensionにするのは不適切。

メソッド

  • 英語の自然言語として読める形式にする
  • 標準フレームワークを真似する
  • イベントのタイミングを表すには助動詞(will, did, should, can)を活用する

設計

  • 標準フレームワークで利用されているパターンを積極的に採用する
  • イベント通知にはdelegate, Blocks, NSNotifcation, KVOを適切に使い分ける

delegate

使いどころ

blockと比べると以下の場合に適している

  • イベントが特定のメソッドに結びつかない場合
  • 通知の種類が多い場合

UIScrollViewDelegateで定義されているような通知はdelegateが適している。 逆にこれをすべてblocksでやったら地獄なのは想像しやすい。

良い使い方
  • 不可欠なメソッドはrequired、不要なケースがあるメソッドはoptionalで定義する。
  • 命名規則を守る(Cocoa向けコーディングガイドライン, p.15)
    • 名前の先頭はメッセージを送信しているオブジェクトのクラスにする
    • will, did, shouldを使ってデリゲートメソッドの役割を明確にする
@protocol ISHCacheDelegate <NSObject>

@optional
- (void)cache:(ISHCache *)cache didRemoveObjects:(NSArray *)removedObjects;

@end

Blocks

  • 変数をキャプチャするので循環参照を生みやすい(仕組みを理解してたとしても)
  • 構文が異常に複雑だが愛があれば問題ない

http://fuckingblocksyntax.com

使いどころ

delegateと比べると以下の場合に適している

  • イベントが特定のメソッドに結びつく場合
  • 通知の種類が少ない場合

特定の処理の完了通知などはBlocksが適している。 NSURLConnectionのsendAsynchronousRequest:queue:completionHandler:とか。

良い使い方
  • 失敗する可能性があるときはBlocksの引数にエラーを入れる(そうしないと利用者は困る)

GameKit.frameworkやSocial.frameworkあたりのAPIはそんな感じのものが多い。

- (void)someHeavyOperationWithHandler:(void (^)(NSError *))handler;

NSNotifcation

以下の場合に適している

  • 通知の受け手が多数の場合
  • 通知の受け手が送信元から参照できない場合

グローバルな通知になるので安易に採用せず、先にdelegate, Blocksで実現できないか検討する。

良い使い方
  • UIKitに絡むような通知はメインスレッドでNSNotifcationをpostする
  • 命名規則を守る(Cocoa向けコーディングガイドライン, p.24)
    [Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification

黒魔術と副作用

Objective-C Runtime APIの話

黒魔術を利用してできること

  • 標準フレームワークの不都合な仕様を変えることができる
  • バージョンの溝を埋めることができる

例: ISRefreshControl

黒魔術はなぜ危険なのか

  • 明示的でないので背景を知らない人には意図しない挙動となる

どうすればいいの?

  • なるべく使わなければいい

単体テスト

単体テストを書く理由

  • ロジックはテストを書いた方が効率的に開発が進む(デモアプリでNSLogデバッグの方が手間がかかる)
  • テストを書いた機能が壊れたことを自動的に検知することができる(逆に、正しいことの証明と考えるべきではない)

アプリのテストコードは仕様変更で簡単にゴミに変わってしまうので、箇所によってはコストとリターンが見合わなくなることも少なくない(特にViewとか)。 しかし、ライブラリは汎用的で変更は少ないので、大抵コストとリターンが見合うことが多い。 ここではアプリの単体テストの話はしないで、ライブラリでの単体テストの話をすることを強調しておく。

どこまで書くべきか

  • 自分が書いたプログラムの分だけテストを書く(FoundationやUIKitに対するテストまで書く必要はない)
  • API毎に網羅的に書くことが望ましい。

どうやって書くの

- (void)setUp
{
    [super setUp];
    
    cache = [[ISHCache alloc] init];
}

- (void)testSettingObjectForKey
{
    id object = [[NSObject alloc] init];
    id key = @"foo";

    [cache setObject:object forKey:key];

    XCTAssertEqual(cache.currentSize, 1U);
    XCTAssertEqualObjects([cache objectForKey:key], object);
}
  • setObject:forKey:の実行時にsizeLimitを超える場合は?
  • setObject:forKey:の実行後にsizeLimitを小さくしたら場合は?
  • removeAllObjectsの実行後のcurrentSizeは?

など、テストできるケースはたくさんある。 (この例でデモアプリで検証するのは大変ということがわかってもらえそう)

CIを導入する

  • "せっかくテストを書いたのに走らせ忘れていつの間にか失敗するようになってた"を防ぐ
  • パッチが来る度にテストを手元で走らせるのは時間の無駄
  • コードカバレッジなどの客観的な指標を測ることが出来る
  • 初めてのセットアップは深刻に面倒だけど、"毎回必ず手元でテストを実行するのを忘れないようにする"というのは現実的ではない
  • "オレの環境で動く/動かない"を防げる

CIを導入するには?

public repositoryにCIを導入するならTravis CIがおすすめ。 Coverallsでコードカバレッジを測定することもできる。

  • CIを導入するにはxcodebuildコマンドでビルドできる狀態にすることが必要
  • Xcode 5からxcodebuildも結構変わったので古い情報に注意

Makefile

test:
        xcodebuild clean test\
                -sdk iphonesimulator \
                -workspace ISDiskCache.xcworkspace \
                -scheme ISDiskCache \
                -configuration Debug \
                -destination "name=iPhone Retina (4-inch),OS=7.0" \
                OBJROOT=build \
                GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES \
                GCC_GENERATE_TEST_COVERAGE_FILES=YES

.travis.yml

rvm: 1.9.3
language: objective-c
script: make test

コードカバレッジも測定している

CocoaPods

foo

リンク

Apple公式ドキュメント

Objective-Cによるプログラミング
https://developer.apple.com/jp/devcenter/ios/library/documentation/ProgrammingWithObjectiveC.pdf

Objective-Cプログラミングの概念
https://developer.apple.com/jp/devcenter/ios/library/documentation/CocoaEncyclopedia.pdf

Cocoa向けコーディングガイドライン
https://developer.apple.com/jp/devcenter/ios/library/documentation/CodingGuidelines.pdf

CI関連

Travis CI
https://travis-ci.org

Coveralls http://coveralls.io

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment