87
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

iOS #2Advent Calendar 2019

Day 12

iOSでもマルチモジュール化したい!

Last updated at Posted at 2019-12-11

この記事は iOS #2 Advent Calendar 2019 の12日目です。
今年の1年間、ぼくが興味を持ち続けてきたiOSアプリのマルチモジュール化について、検討したことをここに書き出します :slight_smile:

マルチモジュール化とは

タイトルからさらっと「マルチモジュール化」という言葉を使ってしまいましたが、ここでは、ライブラリの仕組みを使って複数のモジュールで構成されたiOSアプリにすることを、マルチモジュール化と呼ぶことにします。逆にマルチモジュール化していないアプリをモノリシックなアプリと呼ぶことにします。

ライブラリと言えば、OSSとして公開されている汎用的で再利用可能なものが真っ先に頭に浮かびますが、ここではそういったものを使っているどうかは関係なくて、開発中のアプリの処理を積極的にライブラリとして切り出して(=モジュール化)、そのアプリ内で利用してゆくことを考えます。

イメージ

モジュールの分割方針

モジュールに切り出すと言っても、どこをどう切り出せばいいでしょうか。

ここでSwiftの言語特性を考えてみます。Swiftのアクセスコントロールにはモジュールを境界とするもの(open, public)があるので、モジュールの外に見せる機能とモジュール内だけで隠す実装をコードレベルでコントロールできます。ですから、各モジュールごとに独立した責務を持たせるようにし、アクセスコントロールを使って独立性を高める、という方針で分割すると良いでしょう。

具体的に、どういった単位で分割するのが良いかというのは、アプリの設計と密接に絡む問題…というより設計そのものなので、一般的な答えを出すことはできません。ですが、後に述べるメリット・デメリットを考えながら、いくつかの方向性は考えられます。

機能単位で分割

アプリがある程度の独立した複数の機能から構成されているのなら、その機能単位でモジュールにするというのは直感的でわかりやすい分割方針です。

機能単位での分割例

レイヤー単位で分割

MVVM、MVP、Clean Architecture、VIPER、…などの設計パターンを用いるのなら、そのレイヤー単位でモジュールを分割するのも良いと思います。こういった設計パターンに共通する根本の考え方は、各レイヤーで責務を分離し、それぞれを疎結合にしておくことです。ですから、レイヤーでモジュールを分割するというのは意に適っています。

レイヤー単位での分割例

機能×レイヤー

上記の組み合わせで、大きくは機能で分割し、そこから必要に応じてレイヤーに分割するというのもあると思います。モジュール数が増えることになりますが、アプリの規模が大きいのならそれは自然なことだと思います。逆に規模の小さいアプリで無理して分割するのはオーバーキル感があるので、そこはアプリに合わせて調整が必要です。

いろんな分割例

モジュールに分ける意味のひとつが責務の分割です。責務を分割しておけば、他への影響を最小限に抑えてモジュールを更新できるので、最初は機能で分割しておいて、ある機能の規模が大きくなってきたところで、それをさらに分割するというのも良いと思います。

マルチモジュール化のメリット

ぼくがマルチモジュール化のメリットとして期待している点は、次の3つです。

  • 開発時間の短縮
  • 影響範囲の最小化
  • コンフリクトの減少

開発時間の短縮

モジュールごとに、テストターゲットを用意したり、そのモジュールの動作を確認するための小さなアプリターゲットを用意することで、そのモジュールだけをビルドして開発を行うことができるようになると考えています。

何よりも、モジュールだけのコンパイルで済むのでビルド時間が短縮されますが、テストに関してもそのモジュールのものだけを走らせることでその時間が短縮されます。機能の実装や修正を行う際には、何度もビルド&確認を繰り返すので、アプリの規模がある程度大きくなると、その効果は大きいと思います。

コード変更時の、アプリ全体の差分ビルドも、もしかすると速くなるかもしれないのですが、その点は検証できていません。

影響範囲の最小化

コンポーネントが相互に依存し合っていたりして密に結合した状態になっていると、どこかを変更する際の影響範囲が広がってしまい、変更作業が大きくなったり、気づかないバグを生みやすくなります。

疎結合なコンポーネントの集まりとしてうまく設計していれば、モノリシックなアプリであってもどこかを変更する際の影響範囲を小さくできます。
しかし、本来は特定の機能の実装だけのために作ってあったはずのクラスが他の実装でも使われてしまったり、コンポーネント内でのみ利用するためのメソッドが気づけば外から呼ばれてしまっていたりといった事故は起こり得ます。特に、チームに複数の開発者がいる場合は、意思疎通のズレによってそういうことが起こりやすくなります。

モジュール化すれば、 openpublic 以外のアクセス修飾子を持つクラスやメソッドは、モジュール外には隠されます。アクセス修飾子をつけなければ internal として扱われるので、意識的にアクセス修飾子で指定したものだけがモジュール外に公開されるようになります。
そもそも、そのモジュールをimportしなければ、公開されているものを使うこともできません。
意図せず誤って使ってしまうことがない、ということをコンパイラに保証してもらえるので安心感があります。

また、モジュールが他のモジュールに依存する際には、単方向の依存になります。モジュール間の依存関係もはっきりさせることができます。

コンフリクトの減少

これはやや副次的な効果ですが、モジュールごとにプロジェクトファイルを分けておけば、プロジェクトファイル変更時(ファイル追加など)のコンフリクトが少なくなるのではないかと思っています。

マルチモジュール化のデメリット

もちろん、マルチモジュール化することのデメリットもあります。対象のアプリの機能性・規模によっては、マルチモジュール化のデメリットの方が大きくなる場合もあるので、少なくとも、最初から完璧なものを求めるのはやめた方がいいと思います。

モジュール分割の難しさ

前にも書いたとおり、モジュールをどう分割するかの一般的な解はありません。また、抽象化を突き詰めて分割しすぎると、モジュール同士の依存関係が複雑になって、モジュールを管理するのが難しくなります。
分割しない場合は考えなくてもよかったことを考えなければいけない分、手間が増えるのは間違いないと思います。

抽象度が高くなる分、複雑になる

マルチモジュールのメリットを活用しようとすると、モジュール内の実装は隠して、提供する機能だけを外側に見せるようにしたくなります。つまり、モジュールが提供するインタフェースの抽象度を高くすることになります。

また、モジュールの依存関係は循環しないようにする必要があるので、モジュールが相互に依存しあうことはできません。これを解決するために外側から依存を注入する方法を使うことになるでしょう。
例えば、モジュールAが外部から提供してほしい機能をプロトコルとして定義し、モジュールBでそのプロトコルに準拠したクラスを作成、インスタンス化して、モジュールAに引き渡すというものです。この場合、モジュールAはモジュールBの存在を知らなくてよいので、モジュールAからモジュールBへの依存は必要ありません。

このように抽象度をあげて結合を疎にすることは、実際の処理がどこに実装されているのかがわかりにくくなるという副作用があります。モジュール間を接続するコードも増えるので、必要以上に抽象度を上げることは、ただ単にプログラムを複雑にしてしまう危険性があります。

2つのライブラリの種類

モジュールを作成する際に、Xcodeで新規プロジェクト(または新規ターゲット)としてサクッと作ることのできるライブラリには次の2種類があります。

  • Framework
  • Static Library
ライブラリの種類

Framework

フレームワークは、ライブラリとその他のリソースを1つにまとめたものです。

Dynamicフレームワーク

これを選択して作られるフレームワークにはDynamicライブラリが入っているので、種別としてはDynamicフレームワークです。Dynamicフレームワークは、Embedded Framework(アプリにくっついていくフレームワーク)としてiOS 8から使えるようになりました。
なお、Carthageが扱うのもDynamicフレームワーク、CocoaPodsで use_frameworks! をつけたときもDynamicフレームワークが使われます。

Dynamicライブラリはアプリのメインターゲット(アプリのプログラム)とは別のファイルとして分かれていて、実行時に動的にロードされます。

Dynamicフレームワーク

macOSでは、特定の機能でしか使わないモジュールをDynamicライブラリにすることで、実際に必要になるまでそれをロードしないようにできます。そうすることで起動時間を短縮できます。また、Dynamicライブラリをプラグインのように追加したり差し替えたりすることができます。
しかし、iOSでは起動時にすべてのDynamicライブラリがロードされます。また、ストアに申請するアプリにくっついていく形式しかサポートされていないため、Dynamicライブラリだけを差し替えたりはできません。
ですから、残念ながら、macOSの場合のような利点はありません。

むしろ、Dynamicライブラリの数が多いと逆に起動時間が遅くなります。実際、 WWDC 2016のセッション ではDynamicフレームワークは6個程度にとどめるように案内されています。

So you absolutely can and should use some, but it's good to try to target a limited number, we would, I would say off hand, a good target's about a half a dozen.

翌年の WWDC 2017のセッション でも、できるだけ少なくするようアドバイスされています。

So last year I said do less, and I'm going to say that again this year and I'm always going to say that because the less you do, the faster we can launch.

And no matter how much we speed things up, if we have less work, it's going to go faster.

And the advice is basically the same. You should use fewer dylibs, if you can, you should embed fewer dylibs.

Dynamicフレームワークが使えるようになったiOS 8では、同時にExtension(Today Extensionや、Share Extensionなど)が登場しました。
アプリ本体(メインターゲット)とExtensionで共通する処理を、Dynamicフレームワークにすることにより、アプリサイズの肥大化を抑えられます。おそらく、iOSのDynamicフレームワークはこの使い方を主な目的としているのでしょう。

アプリサイズの削減

モジュールの数が多くなりそうなら、Dynamicフレームワークは避けた方がいいでしょう。

Static Library

SwiftはXcode 6で登場しましたが、SwiftのStaticライブラリはXcode 9になって、やっと作れるようになりました。

Staticライブラリは、ビルド時のリンクフェーズでアプリのメインターゲットに取り込まれます。

Staticライブラリ

このため、Dynamicフレームワークと違って、出来上がる構成はモノリシックなアプリの場合と同じです。起動速度の低下も意識する必要はありません。

ただ、Staticライブラリはフレームワークと違ってライブラリそのものです。つまり、リソースを持つことができません。UIに絡む処理をモジュールで提供したい場合は、Storyboardや画像、文字列など、関連するリソースをモジュールに含めたいところですが、残念なことにそれができません。

リソースについてはメインターゲットに持たせるか、Dynamicフレームワークを使うことになるでしょう。

プロジェクトの構成

モジュールを含むXcodeプロジェクトの構成について考えます。方法はいろいろあるのですが、そのうちのいくつかを紹介します。

1プロジェクト内にターゲットを追加する構成

プロジェクトは1つで、その中に、モジュールのターゲットを追加する構成です。

マルチターゲット構成

シンプルで良い構成だと思います。

この構成で注意したいところは「ターゲットを間違えないこと」です。
ファイルを追加するときに、正しいターゲットにチェックが入っていることを確認したり、ファイルを編集しているときについうっかり右のInspectorでターゲットを変更しないように注意しましょう。

multi_target2.jpg  multi_target3.jpg

なお、この構成ではプロジェクトファイルは1つになるので、メリットとして挙げた「コンフリクトの減少」は期待できません。

これらのターゲット間違いやコンフリクトを避けるために、XcodeGenを使うという解決策が、以下のクックパッドさんのブログで紹介されています。

XcodeGenによる新時代のiOSプロジェクト管理
https://techlife.cookpad.com/entry/2019/04/26/110000

サブプロジェクトの利用

モジュールごとにプロジェクトファイルを分けて、依存するプロジェクトをサブプロジェクトとして参照する構成です。

サブプロジェクト

各モジュールのプロジェクトが分かれているので、モジュール単体のプロジェクトを開いて開発を行うこともできます。
また、モジュール単体でプロジェクトになっているので、アプリのプロジェクトと完全に独立した別のプロジェクトからもそのモジュールをサブプロジェクトとして参照できます。ですから、アプリのプロジェクトには影響を与えずに、そのモジュール専用のテストアプリを作ったりすることもできます。

この構成の注意点は、プロジェクト設定がモジュールごとに分かれることです。アプリ全体でコンパイルオプションを変更したい場合は、それぞれのモジュールのプロジェクト設定を変更してまわる必要があります。

パッケージマネージャの利用

各モジュールをCocoaPods、Carthage、Swift Package Manager(以下SwiftPM)といったパッケージマネージャに対応するパッケージとして開発する方法もできると思います。
CocoaPodsは プライベートなSpec Repoを作ることができますし 、CarthageやSwiftPMでもプライベートなgitリポジトリを参照できるので、OSSとして公開するつもりがなくてもこれらのパッケージを作って使うことは可能です。

しかし、パッケージ開発の手順が必要な分、どうしても煩雑になるので、この選択はちょっと微妙かなと思っています。

ただ、SwiftPMには、近い将来、 Staticライブラリであってもリソースを使えるようになる可能性 があるので、ちょっと期待しています。

まとめ&参考資料

iOSアプリのマルチモジュール化について、

  • モジュールの切り出し方法
  • メリット・デメリット
  • ライブラリの種類の違い
  • プロジェクト構成

という視点で、これまでに検討してきた内容を書きました。
と言っても、検討結果を晒しただけであって、実践はまだそれほど進んでいないのが現状です。実際にマルチモジュール化してみると新たな別の発見があるかもしれないと思っています。

そもそもは、DroidKaigi 2018で「マルチモジュールのすゝめ」というセッションを聞いて、Androidではそういう開発手法の波があるのかと知りました。

今年になって、DroidKaigi 2019の「multi-module Androidアプリケーション」というセッションを見て、iOSでも同じことができないだろうかと考え始めました。このセッションでは40個のモジュールで構成されたプロジェクトが紹介されていました(ちなみに、DroidKaigi 2019ではこの他にもマルチモジュール絡みのセッションが多かった印象)。

その少し後に、Cookpad TechConf 2019で「〜霞が関〜 クックパッドiOSアプリの破壊と創造、そして未来」というセッションがあったことを知り、iOSでもマルチモジュール化の波は来てるじゃん!と確信しました。

ということで、これまでの検討では先人が公開してくれた下記の資料の影響を受けています :slight_smile:

87
49
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
87
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?