24/7 twenty-four seven

iOS/OS X application programing topics.

Swiftで使いやすいAPIを書くために気をつけていること

先日iOSオールスターズ勉強会でSwiftでライブラリを書く際に良いと思ってることについて話しました。そこで好評だったり同意してもらえた何点かについてあらためてまとめます。

オーバーロードを積極的に活用しよう

Swiftではメソッドのオーバーロード(引数の数、型、および戻り値の型が異なる同じ名前のメソッドが定義できる)が言語仕様としてサポートされています。これは上手に使うと利用する側にとってとても書きやすくなるので積極的に使っていきましょう。

例えば下記のような例では、Objective-Cではデータ型によって複数のメソッドを使い分ける必要がありました。 (引数の型がNSStringNSDataかでメソッドの名前が異なる)

- (BOOL)setString:(NSString *)string forKey:(NSString *)key;
- (BOOL)setData:(NSData *)data forKey:(NSString *)key;

Swiftでは次のように書けるので、利用者は型の違いを意識することなく、setメソッドのみを覚えていればよいことになります。

func set(value: String, key: String) -> NSError?
func set(value: NSData, key: String) -> NSError?

下記は引数の数が違う例です。

convenience init()
convenience init(service: String)
convenience init(accessGroup: String)
convenience init(service: String, accessGroup: String)

必須でない引数を省略したメソッドを用意することで、利用するときは次のように必要なときだけ引数を渡せばよいことになります。

let keychain = Keychain()
let keychain = Keychain(service: "com.example.github-token")
let keychain = Keychain(accessGroup: "12ABCD3E4F.shared")
let keychain = Keychain(service: "com.example.github-token", accessGroup: "12ABCD3E4F.shared")

デフォルト引数とオーバーロードならオーバーロードの方がわかりやすい

Swiftではデフォルト引数(引数を指定しなかった場合、あらかじめ指定したデフォルト値が設定される)もサポートされており、オーバーロードと似たような効果を与えることができます。

例えば下記のように3つの引数を取るメソッドのうち、必須でない2つの引数にデフォルト値を与えます。

convenience init(server: String, 
                 protocolType: ProtocolType = .HTTPS, 
                 authenticationType: AuthenticationType = .HTMLForm)

そうすると次のように必須の引数以外は省略して呼び出すことができます。

let keychain = Keychain(server: "https://github.com")

ただ、私はこのような場合はオーバーロードによって同様のことを実現する方が良いと考えています。

理由は2つあって、まず一つは、Xcodeの入力補完を利用する際に、常にすべての引数を取る補完候補が表示されてしまうので、引数がオプションであることがわかりづらい点です。

もう一つは、オプション引数を省略するケースの方が一般的な使い方の場合、補完された引数を消すという行為が必要になるため書くリズムが崩れる、というのがイマイチ使い勝手を悪くすると思うからです。

↑上記のメソッドは呼び出すときにこのように補完されます。引数を省略するには補完された引数をわざわざ消す必要があります。

↑オーバーロードで同じことを実現した場合はこのように補完されます。引数を省略できるメソッドが用意されてることがひと目でわかりますし、どのように書きたい場合でも補完から選ぶことができます。

さらにデフォルト引数として何が与えられているのかという情報も、利用する段階では欠如してしまうので、省略した場合にデフォルト値として何が指定されるのか明示したい、という用途にも今のところ使えないことになります。

convenience init(server: NSURL, 
                 protocolType: KeychainAccess.ProtocolType = default,
                 authenticationType: KeychainAccess.AuthenticationType = default)

↑デフォルト値はこのようにすべてdefaultと表示されるため何が指定されるのか、わからない。

例外: コールバックとしてのクロージャはデフォルト引数で空の実装を与えよう

非同期メソッドのコールバックとしてクロージャを与えるというインターフェースはよくあります。

この場合については、オーバーロードで省略形を用意するよりは、デフォルト引数として何もしないクロージャを与えるのが良いと思います。

func setSharedPassword(password: String,
                       account: String,
                       completion: (error: NSError?) -> () = { e -> () in })

理由としては、非同期メソッドのコールバックは省略することの方が少なそうであること(そうであるなら常に複数のメソッドが補完されるのは面倒)、省略された場合でも呼び出される方でクロージャがnilかどうかをチェックする必要がないのでスッキリ書ける、などです。

keychain.setSharedPassword(password, account: username)
keychain.setSharedPassword(password, account: username) { (error) -> () in
    println(error)
}

↑上記のメソッドはこのようにコールバックを必要に応じて省略できます。省略した場合でもクロージャはデフォルト引数によって必ず渡されるので、呼び出される側の実装は常にクロージャを呼べばよいです。

戻り値のオーバーロードはあまり便利じゃない

Swiftでは戻り値の型だけが異なるメソッドもオーバーロードとして同じ名前で定義できます。

func get(key: String) -> String?
func get(key: String) -> NSData?

しかし上記のようなメソッドを用意した場合、呼び出すときにどちらのメソッドを呼び出しているのか名前だけでは区別できないため、戻り値の型を指定するか、キャストが必要になってしまいます。

↓下のように左辺の変数の型を指定するか、

let data: NSData? = keychain.get("somedata")

↓戻り値のキャストを書く必要がある。

let data = keychain.get("somedata") as NSData?

たいていの場合、気にせず書いていたらコンパイルエラーが出たので直す、ということになってリズムが狂うので良くないと考えています。

私は上記のケースではオーバーロードは使用せずにメソッドの名前を変えることにしました。

func getString(key: String) -> String?
func getData(key: String) -> NSData?

↑スマートではないですが、補完を働かせることもできるのでこちらのほうが良いと考えました。

メソッドチェーンを上手に使おう

Objective-Cの文法ではメソッドチェーンは非常にやりにくいものでした。 (チェーンするなら、まずチェーンする数のカッコを開く必要がある。足りなかったら最初に戻ってカッコを足すことになる。)

Swiftでは自然な形でメソッドチェーンが利用でき、もちろん補完も働くので、有効に使えるケースでは上手に活用しましょう。

keychain
    .accessibility(.AfterFirstUnlock)
    .synchronizable(true)
    .set("01234567-89ab-...", key: "kishikawakatsumi")
keychain
    .accessibility(.WhenUnlocked)
    .set("01234567-89ab-...", key: "kishikawakatsumi")

エラーはEither型として返すことを検討しよう

Objective-Cでは複数の戻り値を返すことはできなかったので、戻り値を返すメソッドでエラーの情報も返す必要がある場合は、NSErrorのダブルポインタを引数に渡す方法が一般的でした。

- (NSString *)stringForKey:(NSString *)key error:(NSError * __autoreleasing *)error;

Swiftでもinoutパラメータを使えば同様のことができますが、errorの変数をわざわざ用意しなくてはいけなかったり、とても使いにくい仕組みなので、別の方法を考えましょう。

単純に戻り値とエラーをタプルで返すというのもアリですが、Swiftには強力なEnumが存在するので、Enumで実装したEither型を返すことをまず検討しましょう。

Either型とはScalaやHaskellなどの関数型プログラミング言語にはたいてい組み込み型として入っている、2つの別個の型の値のどちらかを返す型です。

戻り値としてEither型を返すメソッドのエラー処理は次のように書けます。

let failable = keychain.getStringOrError("kishikawakatsumi")

switch failable {
case .Success:
    println("token: \(failable.value)")
case .Failure:
    println("error: \(failable.error)")
}

基本的にはエラー処理をしてほしいということを型として明示できますし、正常系とエラー系の分岐を自然に書けます。

タプルで戻り値とエラーの両方を返す場合と異なり、正常時のエラー、あるいはエラー時の戻り値といった条件を考慮する必要もなくなります。

非同期メソッドのコールバックではObjective-Cの時と同様にエラーオブジェクトをコールバックの引数で返せばよいかと思います。

keychain.getSharedPassword(username) { (password, error) -> () in
    if error != nil {

    } else {

    }
}

コードを公開する際にはPlaygroundを同梱しよう

ライブラリを公開する際には、READMEなどのほかにPlaygroundをセットで公開すると、すぐに動かして試すことができて非常にわかりやすいのでオススメです。