マイクロマネジメントは悪か?よりよい組織をつくるためのマネジメント形態についての考察

レシピ事業サービス基盤部で部長をやっています、新井(@SpicyCoffee66)です。引越しを機に MtG のカードをほとんど売ったはずなのに、そのときは存在しなかったポケモンカードのデッキが手元にあります。なぜ?

私は 2017 卒のエンジニアとしてクックパッドに入社し、様々な業務を経験した後に 2020 年の 8 月から部長となりました*1。最近はコードを書いていないので Techlife の執筆内容に迷ったのですが、今自分の中にある「優れた組織づくりについての考え方」をまとめてみることとしました。部長になる前にも、グループ長として小規模なチームマネジメントの経験があるとはいえ、それを含めても2年弱のマネージャー経験しか持っていないので、これが絶対の正解というわけではなく一つの考えとして読んでいただけると幸いです。

組織の存在理由

優れた組織づくりについて考えるために、まずは組織の存在理由について考えます。組織の目的については、書籍の数だけ種類が存在するほど多様な記述がありますが、この記事ではシンプルに「ある目標を達成すること」を組織の目的として考えます。多くの会社ではこの目標がミッションになるでしょう。クックパッドで言うと「毎日の料理を楽しみにする」ことが組織の目的となります*2

一般に、目標達成に重要な要素は、目標に向かう主体の "速度" と "方向" になります。したがって、目標に向かう速度と方向について、主体が「個人」であるよりも「組織」である方が優位であることが、組織をつくる理由の根幹となり、また、この優位性の大きい方が優れた組織ということになります。

f:id:spicycoffee:20210726151631p:plain
個人と組織の比較

速度についてはわかりやすいでしょう。人が集まる方がマンパワーが増えるため出力が増加します。また、これまでの組織に存在しない専門性を持ったメンバーが加入することで、組織全体のキャパシティが広がっていきます。

方向については、人数が多くなるほどブレる・間違いが多くなるといった印象を抱く方もいるかもしれません。「船頭多くして船山に上る」という言葉もあります。しかし、人間の認知能力には限界があるため、個人の視野では方向性を決定するための情報や、存在する課題を拾いきれないケースが多くあります。このことから、少なくとも成熟した組織においては、方向性の決定についても個人に対して優位性があると考えられます。

ここまでに、組織で目標に向かうことについて、個人でそれをおこなう際と比較した優位性を確認してきました。この考え方を延長して、優れた組織を

  1. 方向性の決定においてより精度が高く
  2. 決定した方向性に沿ってより速い速度で進むことができる

ものであると定義します。

優れた組織をつくるためのマネジメントについての考察

ここからは、前節の定義に沿って優れた組織をつくるためのマネジメントについて考察していきます。今回は、マネジメント形態の分類として一般に用いられる「マイクロ/マクロマネジメント」という概念をベースに考えを深め、最終的に優れた組織をつくるためのマネジメントについての考察をまとめます。

マネジメント形態の分類

「マイクロ/マクロマネジメント」という形態は、白黒二値にハッキリ分類できるものではなく、各組織でのやり方がグラデーション的に分布している前提のもと、それぞれの特徴を考えます。

マイクロマネジメント

マイクロマネジメントでは、マネージャーがメンバーに対して課題を細かくブレークダウンして伝達し、より詳細・具体の部分にまで指示を出します。そのため、マネージャーからメンバーへのコミュニケーションは作業内容の指示が中心となります。その特性上、施策の方向性がマネージャーの意図からブレることは少なくなりますし、トップダウンの施策決定が多いため、意思決定が速くなる傾向も見られます。

一方で、マネージャーの認知能力が組織の認知能力の限界値となるため、特に方向性決定の精度において、組織であることのメリットを活かしづらい形態であると言えます。課題の解釈や解決方法のアプローチに幅をもたせづらく、マネージャー自身がその設定をミスった際に、組織全体でリカバリーするような自浄作用は働きません。速度についても、詳細にまで細かく指示を出すような極端な例を考えると、マネージャーの想定以上の生産性が発揮されるケースは少なくなります。

また、より具体にまで目を届かせる必要がある以上、マネージャーの目線がより内向き・近視眼的になることも、方向性の決定について精度を落とす要因となります。組織の外や未来のことに目を向ける余裕を持てないと、限られた情報で部分最適に近い選択肢を取ってしまう可能性が高くなるからです。

f:id:spicycoffee:20210726152936p:plain
マイクロマネジメントにおけるマネージャーの意識

マクロマネジメント

対してマクロマネジメントでは、マネージャーはメンバーに現状の課題や目的を伝達するにとどめ、解決へのアプローチについてはメンバーに裁量を持たせます。したがって、マネージャーからメンバーへのコミュニケーションは、課題の共有やより俯瞰した視点での整合性の確認が中心となります。

課題のブレークダウンをなるべく抑えてそのままの粒度でメンバーに伝達することで、マネージャーにより近い粒度で課題を捉えるメンバーが増え、マネージャーの認知能力が組織のボトルネックになりづらくなります。具体的には、マネージャーの課題設定にミスがあった際にメンバーからのフィードバックで再設定が可能になるケースが増えたり、メンバーが独自の観点で課題を解釈することにより、マネージャーが思いつかないような解決策を発案することのできる可能性が高くなります。

より広い範囲の具体についてメンバーに移譲できているため、マネージャーは組織外のことや未来のことに目を向ける余裕ができ、全体最適となる選択肢を取るために必要な情報を集めることができるようになります。

一方で、課題や目的に向き合う視点が増える分、それをまとめ上げるはたらきが必要になります。この整備・工夫を怠ると、意思決定を形成するのに長く時間がかかったり、その方向性がブレる、あるいは全ての意見の間を取った中途半端なものになってしまうといった危険性があります。

f:id:spicycoffee:20210726153003p:plain
マクロマネジメントにおけるマネージャーの意識

マネジメント形態の比較

先ほどまでに述べてきた各マネジメント形態の特徴を以下の表(図)にまとめます。

f:id:spicycoffee:20210726152335p:plain
マネジメント形態の比較

対になる性質は様々ありますが、総じてマイクロマネジメントは、組織の行動における不確実性の低さと意思決定の速度に、マクロマネジメントは、"方向" についての意思決定の精度と実行部分の "速度" に優位性があると言えそうです。ここで一点注意すべきなのは、マイクロマネジメントにおける不確実性の低さはあくまで組織の行動に対してのもの(つまりは内情が把握しやすいということ)であり、目的に対しての不確実性を低減している(つまり正しい方向に進んでいる)ことを保証するものではないということです。同様に、意思決定の速度は組織が進む速度とイコールではなく、間違ったアプローチをとった場合、実行全体で見たときの速度は下がる可能性もあります。

これらのことを考えると、冒頭に定義した優れた組織を実現するためには、マクロマネジメントを採用する方が適していると考えられます。

マクロマネジメントがうまく機能するための条件

前節の比較により、優れた組織の実現のためには、可能であればマクロマネジメントを採用する方が適しているとの考えを得ました。しかし、マクロマネジメントがうまく機能するためには、その特徴から

  • メンバーが組織の目標や状況について正しく理解している
    • メンバーが理解できるように伝達する能力をマネージャーが有しているということでもあります
  • メンバーの実行能力がマネージャーのそれよりも優れている

という二つの条件を満たす必要があります。

前者については、主に組織の進む "方向" に関係する条件です。マクロマネジメントの特徴として、マネージャーの認知能力が組織のボトルネックになりづらくなることを挙げましたが、これは裏を返すと、メンバーがマネージャーの視野の外にある課題や解決策を探索・発見できるという前提に立っています。そのためには、メンバーがマネージャーと同じ解像度で組織の持っている目標や置かれている状況について理解しておく必要があります。一般的には "視座" と表現されることの多い素養ですが、これらの情報を主に握っているのはマネージャーである以上、マネージャーにはこれを自身の意図と合わせて伝達し続けることでメンバーに理解してもらう責任があります。一概に「視座の高さはメンバーの能力である」と捉えるのは間違いで、逆に「マネージャーが情報を下ろしてこないのが悪い」と主張し続けるのも建設的ではなく、双方の歩み寄りによって満たすべき条件であると考えます*3 *4

後者については、主に組織の進む "速度" に関する条件です。マネージャーはマネージャーとして、メンバーはメンバーとしてそれぞれ専門性を持って働いている以上、実行の詳細についてはメンバーに移譲した方が速度が出るのが一般的かとは思います。しかし、中には「プレイヤーから転向したばかりのマネージャー x ジュニアメンバー」といった組み合わせ等、詳細についてもマネージャーから細かく指示を出した方が速度が出るケースもあります。そういった場合、少なくとも短期的にはマイクロマネジメントの方が速度が出ることになるため、メンバーの実行能力が十分に高いということも、マクロマネジメントが優位性を発揮するための条件になります。

より優れた組織をつくるためのマネジメント形態の移行

前節の比較から、より優れた組織を作るためにはマクロマネジメントを採用する方がよいが、それが有効に働くためには組織の成熟度に関する条件が存在することがわかりました。「ジュニアメンバーを中心に構成されている組織において、抽象的な指示しか出さない」「成熟したメンバーが揃っている組織において、詳細についても指示を出し続ける」といった例を考えてみると、それぞれ組織活動が上手くいかないことが想像できるため、直感的にも正しい理解であると言えるでしょう。

この考えを押し進めることで、マイクロマネジメントとマクロマネジメントをマネジメントのステージとして捉え、なるべく マイクロマネジメントからマクロマネジメントへ移行していくことで、より優れた組織をつくる ことができるという考えに至ることができます。組織やメンバーの成熟度によってマネジメントの形態を使い分けながら、なるべくマクロなマネジメントに移行していくことで、組織の生産性を上げることができるという考え方です。現実的にマイクロマネジメントを採用する方が生産性が高くなる状況は存在しますが、それを「組織が成熟していない」状態だと捉える考え方であるとも言えるでしょう*5

f:id:spicycoffee:20210726152420p:plain
マネジメント形態のステージ

マネジメント形態の移行を実現するために必要なことについての考察

前節に提示したマネジメント形態の移行を実現するために、マネージャーとメンバーそれぞれに求められることを考え、記事を締め括ります。

マネージャーに求められること

マネジメント形態の選択と適した速度での移行

組織の舵取りについての責任がマネージャーにある以上、マネジメント形態の選択についても主にはマネージャーの意志のもとおこなわれることになります。組織とメンバーの成熟度を考えながら、どの粒度まで指示を出すべきなのか、どの粒度までの権限移譲が可能なのかを考え、メンバーとコミュニケーションを取る。その過程で少しずつ権限の移譲範囲を広げていき、よりマクロなマネジメント形態への移行を実現することが求められます。文字にするのは簡単ですが実行は非常に難しく、私も「移譲したつもりで詳細の把握を怠っていたら手戻りにつながった」など、これまで何度も失敗してきました。マネージャーに求められる能力は多様なものがありますが、この判断精度を上げることは、明確にマネージャーとしての成長につながると思っています。

メンバーの成長支援

"マネジメント形態の比較"節において、マクロマネジメントがより有効性を発揮するための条件として、

  • メンバーが組織の目標や状況について正しく理解している
  • メンバーの実行能力がマネージャーのそれよりも優れている

の二点をあげました。つまり、マネジメント形態の移行を実現するためには、マネージャーとしてこの二点にコミットする必要があるということになります。メンバーが成長するとうれしいといったような心理的な要因をあえて除き、組織論の観点からドライに考えたとしても、メンバーの成長を支援する理由がマネージャーには存在するのです。

前者については、組織の目標やその意義、置かれている状況について繰り返し伝達する必要があります。組織の定例、普段の雑談、メンバーの提案した施策 issue へのコメント等、折に触れて伝え続けることが重要です。特にメンバーの提案をリジェクトする場合、その原因となるマネージャーとメンバーの観点や持っている情報の差について丁寧に説明することで、お互いの思考を同期する機会とすることが可能です。

後者については、メンバーの成長になるような挑戦の機会を業務の中で設計したり、自己研鑽のための投資を推奨することが効果的になります。

どちらについても、実行におけるミスや足元の速度低下についてある程度のリスクを飲む必要はあるでしょう。メンバーの成長支援が中長期的な投資であることをしっかりと認識し、提供する挑戦機会やスケジュールのバッファなどを通して、リスクコントロールをすることもマネージャーの役割であると考えます。

メンバーに求められること

自身の成長

マネジメント形態の移行についてメンバーに求められることは、何よりも自身の成長になります。自身の成長が組織の成熟につながることをしっかりと認識し、能力を高めていくこと。もう一歩踏み込んだ話をすると、組織の成熟につながるような成長を目指すことが求められます*6。その成長についても、速度を上げるための実現能力の成長はもちろんのこと、方向性の精度を上げるための視野・視座についても、より広く、より高いものにしていくことが望ましいと考えられます。マネージャーから伝達された課題や状況といった情報を一旦咀嚼し、理解しきれなかったところは積極的に議論を持ちかけるなど、メンバーからの歩み寄りも重要になります。特に自分の提案がリジェクトされたタイミングにおいては「マネージャーの理解と自分の理解との間に大きな差がある」ことがほぼ確定するので、その差について積極的に議論し、吸収することが求められます。

最後に

この記事では、最初に組織の存在理由と優れた組織について定義し、それを実現するための方法を、組織の観点からマネジメント形態と、個人の観点からメンバー・マネージャーにそれぞれ求められることの両方について考えました。私自身は、組織を良くするのもあくまでより大きな価値をより早く提供するためというスタンスを持っていますが、同時に健全なプロダクトは健全な組織からしか生まれないという信念も持っています。

冒頭にも述べたように、この記事で述べたことが正解であるというつもりはないので、気になること等がありましたら是非 Twitter 等でコメントをいただけるとうれしいです。また、Meety に以下のカジュアル面談を掲載していますので「直接聞いてみたいことがある」「クックパッドに興味がある」といった方は気軽にご応募ください。 

meety.net

クックパッドでは、チーム開発で大きな価値を世に届けたい開発者を絶賛大募集中です。転職を考えている方は以下の採用サイトから、そこまでではないがクックパッドに興味が出てきた方は、上記のカジュアル面談から是非ご連絡いただけますと幸いです。 

info.cookpad.com

*1:過去の仕事から生まれた記事はこちら → spicycoffee66 の検索結果 - クックパッド開発者ブログ

*2:この辺りの組織の意義や構造に関する話は『すぐれた組織の意思決定』という書籍も参考になるかと思います

*3:"視座" については 専門職と視座. こんにちは。ミクシィでスポーツやライブエンタメ関連の技術部長を担当している石井で… | by Kunzo Ishii | mixi developers | Medium も参考になります

*4:視座は能力だけの問題じゃないよというような話は オープンでフラットな組織が突然「閉鎖的」と言われるとき|柴田史郎|note にも

*5:組織形態を「ステージ」として捉えるという考え方は『ティール組織』に出てくる考え方に近い発想かもしれません

*6:ここでは「個人の市場価値を高める」という目的については述べていません

コード生成を用いたiOSアプリマルチモジュール化のための依存解決

こんにちは、モバイル基盤部の@giginetです。

iOS版のクックパッドアプリでは、2019年頃より、大規模なアプリを複数のモジュールに分割するマルチモジュールの導入を進めてきました。

今回はクックパッドアプリのマルチモジュール化の戦略について、主に依存関係の解決という点に焦点を当てて紹介します。

クックパッドアプリとマルチモジュールプロジェクト

iOS版のクックパッドアプリはコード量が多く、膨大なビルド時間が問題となっていました。また、同時に関わる開発者も多く、それぞれの機能間を疎結合にしたいという需要が大きくありました。

この問題を解決するために、2019年の初頭からiOSアプリのマルチモジュール化プロジェクト*1を開始しました。 以来、ここ2年で、モジュール分離を前提とした開発が大きく進みました。

現在では、アプリ全体のコードのうち、半分以上がモジュール分離され、アプリ全体が約25個のモジュールに分割されています。

これまでのマルチモジュールの取り組みは、2019年に開催されたCookpad Tech Conf 2019の講演「〜霞が関〜 クックパッドiOSアプリの破壊と創造、そして未来」で紹介しています。

以下は上記の講演動画の書き起こし記事です。この記事を読む前にご覧いただけると、より理解しやすくなると思います。

モジュール分離には、分割の方法や粒度、移行プロセスなど、様々なトピックがありますが、この記事ではとりわけ、大きな障害となるモジュール間の相互の依存関係解決のための仕組みについてお伝えします。

クックパッドアプリのモジュール構成

まず始めに、前提となるアプリケーション全体の構成について説明します。

クックパッドアプリのモジュール構成は概ね以下のような図で表すことができます。

f:id:gigi-net:20210615192428j:plain
クックパッドアプリのモジュール構成

下の層はCoreモジュールと呼ばれています。図中でCookpadCoreやCookpadComponentと呼ばれているモジュールです。 Coreモジュールは抽象化のためのインターフェイスや、アプリ内で共通して使うUIコンポーネントを提供しています。

その上の層はFeature Moduleと呼ばれる層です。検索、レシピ投稿、つくれぽ、買い物機能など、機能単位をひとまとまりとするモジュールに分離されています。 図中にある黄色い四角は、シーンと呼ばれる単位です。アプリ内の1画面に相当します。 各シーンは、VIPERアーキテクチャにより実装されています。Feature Moduleは、ドメイン層を共有する複数の(数個の)シーンの集まりということができます。

f:id:gigi-net:20210615192457p:plain
Feature ModuleにおけるVIPERアプリケーションの構成

最上位のCookpadはアプリケーションです。 Xcodeプロジェクトにおける、アプリケーションターゲットに相当します。アプリケーションは全てのモジュールの実装を知ることができます。

単機能ビルドを重視したモジュール構成

モジュール分離の議論でしばしば話題に挙がるのが、アプリケーション全体をどのように分割し、再構成するかという点です。

我々のモジュール分割の大目的はビルド時間の削減にありました。

そのため、アプリ全体をビルドせずとも開発できるように、モジュール1つを単体起動できる構造を重視しています。

このような構成になっていることで、Feature Module単体のみを取り出し、個別にビルドすることが可能となりました。

この構成を生かした結果が、Sandboxアプリという動作確認用のミニアプリです。Feature Aの開発を行う場合は、CoreモジュールとFeature Aのビルドのみで動作確認ができるようになりました。

f:id:gigi-net:20210615192522p:plain
Sandboxアプリの構成

これにより、単機能のみのビルドと実行を行うことができるようになり、開発効率が大きく改善されています。

詳しくは以下の記事で紹介しています。併せてご覧ください。

マルチモジュール化における依存関係解決の必要性

巨大なアプリを複数のモジュールに分割する際に問題になるのは、依存関係の解決です。

f:id:gigi-net:20210615192553p:plain
Feature Module同士の参照は制限されている

我々のアーキテクチャでは、同じレイヤー上のモジュールが、お互いに参照を持つことを制限しています。もし両方向に依存してしまうと即座に循環参照が発生してしまうからです。その代わり、循環参照を避けるための依存抽象化の仕組みを導入しています。

複数のFeature Moduleが互いに連携する例を考えてみましょう。 例えばBモジュールで実装されているシーンを、Aモジュールで表示するというのが代表的な例です。

これを実現するために、どのような抽象化を実現しているのでしょう。

Environment 〜モジュール外への依存を簡単に注入できるためにするコンテナ〜

各シーンはEnvironmentという、依存関係を取り出すためのDIコンテナにアクセスすることができます。

Environmentは、外部とのI/Oや外部ライブラリ、モジュール間の連携といった、別のモジュールとの連携を持つ必要がある実装を抽象化する役目を果たしています。 各シーンは、他のモジュールの実装を用いたいときや、ネットワーク通信など、外部との入出力が発生するとき、Environmentを用いて依存にアクセスします。

アプリケーションターゲットは、Environmentに具体的な実装を注入する役目を持ちます。

f:id:gigi-net:20210615192615p:plain
Environmentを使った依存関係解決の仕組み

Environmentは動作環境によって差し替えることができます。例えばアプリ起動時やテスト開始時など、環境の起動時に生成して全てのシーンに渡されます。 これにより、フルビルドしたアプリケーション、Sandboxアプリ、ユニットテスト、UIテストなど、様々な環境で異なるEnvironmentを用意することで、依存の注入を簡略化しています。

ResolverとDescriptor 〜依存を抽象化して取り出すための仕組み〜

先に述べたとおり、それぞれのFeature Moduleはお互いを知ることができないため、他のモジュールの実装を取り出すためには、Environmentを経由する必要があります。 この仕組みをResolverと呼んでいます。

同時に、Resolverに特定の実装を示すためのマーカーをDescriptorと呼んでいます。このDescriptorはCoreモジュールに存在します。 そのため、あるモジュールは他のモジュールにある実装を知ることができませんが、その実装を示すDescriptorはどこからでも知ることができます。

他のモジュール上にある実装を取り出したい場合、Descriptorを用いて、Environmentに実装の取得を要求します。 アプリケーションターゲットは全てのモジュール上の実装を知っていることを利用し、アプリケーションターゲットは具体的な実装をEnvironmentに注入し、protocolで抽象化して返します。 その結果、実装を取り出したいモジュールからは、protocolを用いて抽象化された実装を取り出すことができます。

開発しやすいResolverのための課題

このResolverの仕組みを、シンプルに、ミス無く維持するためには何が必要でしょうか。大きく、以下のような点が議論に挙がりました。

型安全性

Resolverから取得した値を任意の型として型安全に使いたい

コンパイル時の網羅性の検証

あるDescriptorに対応したResolverの実装漏れを自動的に検出できるようにしたい

利用の簡便さ

開発時のグルーコードの記述量を減らしたい。また、実装に迷わないようにしたい

コード生成を用いた型安全なResolver

これらを満たすために、コード生成を用いてResolverをある程度自動生成する仕組みを実現しました。

ここからは、その仕組みを使った、モジュール間依存解決の例を見ていきましょう。

Coreモジュール

まず、開発者はDescriptor構造体をCoreモジュールに定義します。

これは、レシピIDを元に、該当のレシピ詳細シーン(RecipeDetails)のUIViewControllerを取り出すためのDescriptorです。

extension ViewDescriptor {
    public struct RecipeDetailsDescriptor: TypedDescriptor {
        public typealias Output = UIViewController
        public var recipeID: Int64

        public init(recipeID: Int64) {
            self.recipeID = recipeID
        }
    }
}

Descriptor構造体は、依存を取り出すために必要なパラメータと、取りだしたい依存の型(Output)を持ちます。

コードの自動生成

ビルドシステムは、ビルド時に、Coreモジュールに含まれるDescriptorを探索し、それらを使うResolverを実装するように促すprotocol、ConcreteViewResolverを自動生成します。 ConcreteViewResolverは、定義されている全てのDescriptorに関するresolveメソッドを持つprotocolです。

// 自動生成される
public protocol ConcreteViewResolver {
    func resolveConcrete(_ descriptor: ViewDescriptor.RecipeDetailsDescriptor) -> ViewDescriptor.RecipeDetailsDescriptor.Output
}

コード生成にはSourceryという、Swiftのコード生成を行うユーティリティを利用しています。

この仕組みにより、存在する全てのDescriptorに対してのResolverの実装が、コンパイル時に強制されるため、Resolverの実装漏れを防ぐことができます。

アプリケーションターゲット(Cookpad)

次に、開発者はアプリケーションターゲットにおいて、ConcreteViewResolverに適合したEnvironmentを実装します。

extension CookpadEnvironment: ConcreteViewResolver {
    func resolveConcrete(_ descriptor: ViewDescriptor.RecipeDetailsDescriptor) -> ViewDescriptor.RecipeDetailsDescriptor.Output {
        RecipeDetailsViewBuilder.build(with: descriptor, environment: self) // レシピ詳細シーンを生成する
    }
}
利用したいモジュール

最後に利用したいモジュールからResolver経由で実装を取り出します。

let destinationViewController: UIViewController = environment.resolve(ViewDescriptor.RecipeDetailsDescriptor(recipeID: 42))
currentViewController.present(destinationViewController, animated: true, completion: nil)

このとき、取り出した型はDescriptor.Outputに指定した型にダウンキャストされます。これにより、利用者は型安全に他のモジュールの実装を取り出すことができます。

型安全に実装を取り出すための仕組み

ところで、上記のようなダウンキャストはどのように動作するのでしょうか。これも自動生成されたextensionで実現されています。

ConcreteViewResolver protocolを生成するタイミングで、Environmentの方には、任意のDescriptorをダウンキャストするresolveメソッドが自動生成されます。

このメソッドは、先ほど実装したresolveConcreteをそれぞれのDescriptorごとに呼び出し、Descriptor.Outputにダウンキャストします。

アプリケーションターゲット(Cookpad)
// このコードも自動生成される
extension CookpadEnvironment {
    func resolve<Descriptor: TypedDescriptor>(_ descriptor: Descriptor) -> Descriptor.Output {
        switch descriptor {
        case let recipeDetailsDescriptor as ViewDescriptor.RecipeDetailsDescriptor:
            return resolveConcrete(recipeDetailsDescriptor) as! Descriptor.Output
        case let anotherDescriptor as ViewDescriptor.AnotherDescriptor:
            return resolveConcrete(anotherDescriptor) as! Descriptor.Output
        // 以下省略...
        default:
            // 全てのケースをコード生成で網羅しているので、ここには到達し得ないはず
            fatalError("Unknown descriptor!")
        }
    }
}

この巨大なswitch文をコード生成することで、存在する全てのDescriptorがケースとして網羅されます。 これにより、コンパイル時に、全てのDescriptorに対するResolverの実装が保証されるのです。

型安全なインターフェイスの取得

この際、実際にResolverから返されるViewControllerは RecipeDetailsViewController ですが、これを取得した他のモジュールからは単なるUIViewControllerとして見えます。

これは、RecipeDetailsViewControllerを含むモジュール内でしか、この具体的な型を知ることはできないためです。

各ViewControllerに機能を持たせたい場合、この仕組みにより、任意のインターフェイスを公開し、取得することもできます。

Coreモジュール(CookpadCore)

Coreモジュール内に、ViewControllerのうち、公開したいインターフェイスのみを含むprotocolを追加しましょう。

public protocol RecipeDetailsViewControllerProtocol {
    var recipeID: Int64 { get }
}

このとき、具体的な実装はこのViewControllerが実装されているFeature Module内に存在します。Coreモジュールは実装を持っていません。

その後、先ほどのようにDescriptorを実装します。このとき、Outputの型としてRecipeDetailsViewControllerProtocolを返すように指定しています。

extension ViewDescriptor {
    public struct RecipeDetailsDescriptor: TypedDescriptor {
        public typealias Output = UIViewController & RecipeDetailsViewControllerProtocol

        // ...
}
利用したいモジュール

同様に利用したいモジュールからはresolverを用いて実装にアクセスできます。このとき、返却される型は Descriptor.Output に指定した型として扱われます。

let destinationViewController: UIViewController & RecipeDetailsViewControllerProtocol = environment.resolve(ViewDescriptor.RecipeDetailsDescriptor(recipeID: 42))
destinationViewController.recipeID // 42

これにより、他のモジュールはResolver経由で実装を任意のprotocolに適合した形として取り出すことができました。


このように、Resolverの実装にコード生成を用いることで、型安全性を保ちながら、コンパイル時に網羅性を担保し、実装ミスが起こらないようにすることができました。

また、開発者はDescriptorと、それに対応するResolverを用意するだけでよいので、Resolverの実装コストを減らすこともできました。

ドメイン層へのDescriptorの拡張

今回の例では、簡単のため、画面遷移にResolverを使う例をご紹介しました。

我々のアプリでは、DataStoreやUseCaseといった、ドメイン層の依存解決のためにもResolverを解放しています。

例えばAモジュールで実装されているUseCaseをBモジュール内の実装で扱いたいという例です。 Viewの例と同様に、公開したいインターフェイスのみをprotocolとして抽象化し、DataStoreDescriptorとして同様の仕組みを実現しています。

この仕組みにより、モジュール間での任意の依存解決を簡単に実現できるようになりました。

さらにマルチモジュールについて知りたい方へ

今回の記事では一部しか紹介することができませんでしたが、いくつかの記事やイベントで、クックパッドアプリのマルチモジュール戦略についてご紹介しています。

冒頭に紹介した「Cookpad TechConf 2019 〜霞が関〜 クックパッドiOSアプリの破壊と創造、そして未来」では、今回紹介したマルチモジュール戦略の全体像を説明しています。

先日、5/26には、「Cookpad Lounge #3 クックパッド iOS アプリを爆速で開発できるようにする話」というイベントで、座談会形式でマルチモジュールの話をしました。 視聴者の方から多くの質問を頂き、ありがとうございました。アーカイブが以下で公開されています。

来る6/16 19:30より、メルペイさんが主催の「iOS Tech Talk 〜 Multi module 戦略座談会 〜」というイベントで、各社のマルチモジュール戦略について座談会を行います。 この中でもクックパッドアプリのマルチモジュール戦略についてご紹介するつもりです。ご興味のある方はぜひ遊びに来てください。

まとめ

今回は、クックパッドアプリのマルチモジュール化のうち、特に依存関係解決の仕組みにフォーカスしてご紹介しました。

マルチモジュールは、iOS界隈でも関心度の高いトピックなため、今後も発信の機会を作っていきます。

また、ご質問がある場合は@giginet までお気軽にお寄せください😄

クックパッドでは、大規模なアプリのリアーキテクチャを行いたいエンジニアを募集しています。

*1:霞が関プロジェクトと呼んでいます

データ分析 SQL とその実行結果を共有・検索できるアプリ Bdash Server を作りました

こんにちは。クックパッドでエンジニアをしている @morishin です。Bdash Server というデータ分析 SQL を共有するアプリケーションを作って社内で使い始めたのでその紹介をします。

クックパッドのサービス開発は「仮説を立てる」→「作ってリリース」→「効果検証」の繰り返しで進んでいます。ここで言う効果検証というのは作ったサービスが狙い通りの使われ方をし、ユーザーに価値を提供できているかどうかの確認のことです。その手段は複数あり、実際に使っていただいたユーザーさんにインタビューをさせていただく場合もあればアプリケーションから送信されたアクセスログ等を分析することで評価する場合もあります。この記事では後者の定量分析を効率化するためのツールを作った話をします。

続きを読む