iOS アプリを開発していて、「このクラスは public や internal のようなプロジェクト全体ではなく、特定のファイルやディレクトリだけからアクセスさせたい。ただ、private や fileprivate では実現できないから、仕方なく internal に設定してる。ソースコードに利用注意のコメントで書いて、開発者やレビューでアクセス範囲を担保する。」というシーンに出会ったことはありませんか?
internal だけどアクセス範囲は絞りたい
たとえば FeatureAPI という構造体があり、これを作るために一部機能を RestrictedAPI に切り出したとします。RestrictedAPI は FeatureAPI のためだけにあり、他からの呼び出しは想定していません。しかし、Xcode のコード補完などで候補にあがり、意図せず他で利用されるかもしれません。
/// 任意な機能を実行する
struct FeatureAPI {
private let api = RestrictedAPI()
}
/// FeatureAPI を作るための構造体。他の構造体やクラスには利用用途がないので、アクセスされたくない
struct RestrictedAPI {
}
では、どうすればいいか。解決策は簡単で fileprivate で RestrictedAPI のアクセスを制限すればよいです。
/// 任意な機能を実行する API
struct FeatureAPI {
private let api = RestrictedAPI()
}
/// FeatureAPI を作るための構造体。他の構造体やクラスには利用用とがないので、アクセスされたくない
- struct RestrictedAPI {
+ fileprivate struct RestrictedAPI {
}
これは私もよく利用する方法ですが、腑に落ちないところもあります。なるべく1ファイル1クラス(構造体)で作りたい。処理系が複数あることでコード量も多くなり、ファイルの見通しが悪い。また、機能実装を分けている時点で、ある程度の独立性はあるので、ファイル分割したい!と。
ここでファイルを分割するとアクセス権の問題が再燃します。fileprivate は使えなくなるので、internal に設定し直します。
/// 任意な機能を実行する API
struct FeatureAPI {
private let api = RestrictedAPI()
}
/// FeatureAPI の他では使わないで!
struct RestrictedAPI {
}
他から利用されないために、コメントを添えるのが精一杯です。なお、SPM でモジュール化するという手段もありますが、今回の例ではオーバーキル感があるので採用しません。
他の言語では?
JavaScript の lint ツールである eslint には、同様な問題を解決するプラグイン eslint-plugin-import-access があります。私も JavaScript / TypeScript を書くときは導入していて、とても重宝しています。これを Swift の lint ツールでも再現できないかな…?
SwiftLint のカスタムルールで制御する
今回の lint ツールとして SwiftLint 0.57.1 を利用します。SwiftLint はカスタムルールで自作のルールを追加できるので、そのカスタムルールで制御してみました。
制御対象のディレクトリ構造を次のようにします。CoreAPI の配下に、それぞれの機能の API が配置されます。その機能 API の1つである FeatureAPI は同ディレクトリに配置された RestrictedAPI を利用します。RestrictedAPI は FeatureAPI からのみ呼ばれるものとして、CoreAPI から直接呼ばれることは想定していません。
SampleRestrictedFileAccess/
┗ CoreAPI/
┣ CoreAPI.swift
┗ FeatureAPI/
┣ FeatureAPI.swift
┗ RestrictedAPI.swift (※利用想定は FeatureAPI.swift のみ)
この RestrictedAPI のアクセスをカスタムルールで制御します。作成したルールは単純で、他で利用されたくない RestrictedAPI をプロジェクト全体で禁止(エラー)に設定して、利用したいディレクトリのみ例外として利用許可しました。
custom_rules:
restricted_api_usage:
name: "Restricted API Usage"
regex: "\\bRestrictedAPI\\b"
message: "RestrictedAPI は指定されたディレクトリ外では利用できません。"
severity: error
included:
- SampleRestrictedFileAccess/
excluded:
- SampleRestrictedFileAccess/CoreAPI/FeatureAPI/
このルールを実行すると、実際に利用したい場所では使えて、利用したくない場所ではエラーになりました。カスタムルールでアクセス制御は実現できました。
ただし、特定ワードとして、大雑把に判定しているので、コメント文でも検出されました。regex で設定する正規表現に修正の余地があります。
\スーパーマサカリゾーン/
カスタムルールで特定ディレクトリのみのアクセス制御はできましたが、正直なところ、イケてないです。
このやり方はカスタムルールを毎回かつ個別に設定する必要があり、手間がかかります。また、リファクタリングなどでディレクトリ構造が変更されたらカスタムルールの修正も必要になると、安定性に欠けます。
参考に挙げた eslint-plugin-import-access と比べると完全度は低いです。このやり方は間違っている!、もっといい方法があるぞ!という方はマサカリ(コメント)をお願いします。
まとめ
あまり完成度は低いですが、Xcode で特定ディレクトリだけアクセスできるようになりました。改めて、もっと良い方法があれば、教えてもらえると幸いです。今回試したサンプルは GitHub に公開しています。ご興味あれば、ご確認ください。