Help us understand the problem. What is going on with this article?

【Swift】Extensionの定義の仕方について

More than 3 years have passed since last update.

アプリのような比較的大きなプロジェクトにおけるExtensionの定義の仕方について考えてみました。
まだ実践したわけではないのですが、一旦まとまったので共有してみます。

定義の仕方

  • 一つのファイルで拡張する既存型は一つにする。
  • 一つのファイル内でExtensionを複数定義してもよい。
  • 追加される型・メソッドで関連するものは、共通のExtensionのスコープ内でグルーピングする。
  • ファイル名は <拡張する既存型名>+<分類名>.swift にする。
  • プロジェクトに<拡張する既存型名>グループを作成してファイルをまとめる。
    • プロジェクト外で定義された既存型に関するグループはわかりやすい場所にまとめる。
    • プロジェクト内で定義された既存型に関するグループは、その型が定義されたファイルもその下に置く。

プロジェクトの構成例。

extension_files.png

Extensionファイル例。特に依存関係にあるメソッドは同じスコープに入れると良さそうです。

String+ConsecutiveCharacters.swift
extension String {

    // repeatCountの数だけ文字列中の"!"を連続させた文字列を返すメソッド
    // ex) "Hello!".addConsecutiveExclamation(5) -> "Hello!!!!!"
    func addConsecutiveExclamation(repeatCount: UInt) -> String {
        return addConsecutiveCharacters("!" as Character, repeatCount: repeatCount)
    }

    // 引数cの文字をrepeatCountだけ連続させた文字列を返すメソッド
    func addConsecutiveCharacters(c: Character, repeatCount: UInt) -> String {
        return self.characters.reduce("") { str, char in
            var newStr = str
            let rc = (c == char) ? repeatCount : 1
            for _ in 0..<rc {
                newStr.append(char)
            }
            return newStr
        }
    }

}

extension String {

    // 連続するスペースを1つにまとめた文字列を返すメソッド
    // ex) "Hello     World".removeConsecutiveSpace() -> "Hello World"
    func removeConsecutiveSpace() -> String {
        return removeConsecutiveCharacters(" " as Character)
    }

    // 連続する引数の文字を1つにまとめた文字列を返すメソッド
    func removeConsecutiveCharacters(c: Character) -> String {
        return self.characters.reduce("") { str, char in
            if let last = str.characters.last where char == c && last == char {
                return str
            } else {
                var newStr = str
                newStr.append(char)
                return newStr
            }
        }
    }

}

そもそもExtensionについて考え出した背景

以前Alamofireのソースコードを読んでいたときに、Extensionを定義する方法が気になりました。

例えばAlamofire/AlamofireresponseJSONを呼ぶ例が記載されています。

Alamofire.request(.GET, "https://httpbin.org/get")
         .responseJSON { response in
             debugPrint(response)
         }

「このresponseJSONの実装はどうなってるのかな?」と思い、requestメソッドの戻り値の型がRequestなので Request.swift を見るのですが、responseJSONは見当たりません。「あ、Extensionなのか。多分 Response.swift あたりに定義されてそうだな。」と思って覗いてみましたが、ここにもありません。

正解は ResponseSerialization.swift でした。

このとき気づいたのですが、よく見るとRequest型のExtensionはAlamofireの至るところに存在しており、実際ビルドされたときにRequestがどうなっているのかよくわかりません。このようなExtensionの定義方法は、Alamofireのような小さいライブラリなら良いものの(むしろ巧みにextensionを使っていて勉強になります)、大きなアプリを作るときは困りそうだと感じました。

Alamofire的なExtension定義の特徴

  • 追加される型・メソッドで関連するものは、一つのExtensionのスコープ内で定義される。
  • Extensionは分類ごとにファイル分けされている。
  • ファイル名は <分類名>.swift となっており、このファイルによってどの既存型が拡張されるかは中身を見ないと分からない。
  • 一つのファイルに複数の既存型に対するExtensionが定義されることもある。

良いと思った点

  • 追加される型・メソッドがExtensionのスコープによってグルーピングされているため、関連がわかりやすい。

困りそうな点

  • 目的の定義がどこにあるか分かりにくく、ソースコードが追いにくい。特にGitHub/GitLabなどにおいてWebブラウザでコードレビューしたりするときに困る。
  • チーム開発の際は何かルールを決めないと、いろんなファイルにExtensionができてカオスになりそう。

Extensionを管理しやすくするために

上記内容を踏まえつつ、管理しやすいExtensionにするために次のようなことを考えました。

  • どの既存型に対するExtensionかはファイル名で判別できるようにしたい。
  • 同じ既存型に対するファイルは同一箇所にまとめるようにしたい。

これらを踏まえたのが冒頭で述べた定義の仕方になります。

まとめ

アプリプロジェクトのような比較的大きなプロジェクトにおけるExtensionの定義の仕方について書きました。
結果的にObjective-Cのときと同じような管理になりましたが、これが良さそうに思います。

もし他に良い案があればコメントいただけるとうれしいです。

参考

Swift で Extension につけるファイル名のベストプラクティス | FIVETEESIXONE

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away