こんにちは、株式会社AbemaTVの安井です。業務ではiOSエンジニアとしてABEMAアプリの機能開発を行いつつ、Androidエンジニアとともにモバイルアプリの設計を見直していく “リアーキテクチャ” に携わっています。
先日サイバーエージェントで開催されたテックカンファレンス「CA BASE NEXT」において、私たちのリアーキテクチャの取り組みについて「ABEMA モバイルアプリにおけるリアーキテクチャの取り組みと展望」というタイトルで発表させていただきました。こちらの記事では、時間の関係上ご説明できなかった点も交えながら、改めて私たちの取り組みについてご紹介します。
リアーキテクチャに至る背景
具体的にどういったアーキテクチャを目指しているかをご紹介する前に、まずこの取り組みに至った背景について説明させていただきたいと思います。
ABEMAでリアーキテクチャの取り組みが始まった要因のひとつに、アプリ開発チームにおける体制変更があります。弊社では従来AndroidチームとiOSチームがそれぞれ存在しましたが、2019年10月からは新たに「Nativeチーム」として統合され、Androidエンジニア・iOSエンジニアが同じチームとして開発を行う体制となりました。これにより、各エンジニアが事業の状況や本人の希望に応じて担当するプラットフォームを変更しやすくなったり、他のチームとのコミュニケーションを一元化して効率的に行えるようになったりと、 “モバイルアプリ” という一領域を担当するチームとしてより一体感を持ってプロジェクトを進行させることが可能となりました。
開発を一体となって進められることにより、これまで発生してしまっていたプラットフォームごとの仕様差異や細かな実装の違いも積極的に防ぎやすくなりました。しかし、実際に両プラットフォームの挙動を厳密に合わせていくにあたっては、両プラットフォームの実装におけるアーキテクチャの差が課題となりました。
既存の実装では、どちらのプラットフォームでも仕様として共通化されているべきロジック、いわゆる “ビジネスロジック” と、各プラットフォーム固有のUIなどについてのロジックが、明確には分離されていませんでした。また、両プラットフォームともに共通してFluxアーキテクチャを採用していたものの、例えばiOSではMVVMを併用している一方Androidではしていないなど、各コンポーネントの責務や単位が一致していなかったために、全く同じ仕様だったとしても必ずしも同じように実装できるとは限らない状態でした。
こうした中で、Fluxで実装されたデータフローの複雑さやiOSアプリにおける設計の混在といった実装上の課題にも後押しされながら、両プラットフォームで共通化すべきビジネスロジックが明確に分離され、同じように実装することができるようなアーキテクチャへの移行を目指すこととなりました。
共通化すべき仕様に対応する実装とそれ以外が明確に分離された設計を目指すという背景から、まずClean Architectureを土台として議論を始めることにしました。設計のイメージを具体化していく上では、AndroidとiOSのエンジニアがそれぞれ2名程度、一部メンバーは他の機能開発と兼務する形でリアーキテクチャに携わり、毎週時間を設けて議論を行いました。また、具体化していった設計の全体像や方針を随時Nativeチーム全体に共有し、それに対するフィードバックをスプレッドシートに記入してもらいながら、その内容も基として議論を深めていきました。なお、どれだけカジュアルな、煮詰まっていない疑問や意見でも気兼ねなく書きやすいようにと用意したこのスプレッドシートは、現在はAndroid・iOS横断のGitHubリポジトリのIssues機能に役目を譲っています。
こうした議論を経て、現在ABEMAのAndroid・iOSアプリでは以下のスライド中の図のような形になりました。まず、Android・iOSで最低限守るべきアーキテクチャのルールを、いくつかの “レイヤー” とその依存関係として定義しています。そして、このレイヤーの関係性を維持したまま、各プラットフォームでレイヤーごとに1個以上の “モジュール” を定義します。各モジュールは、その役割に応じて必ずどこかのレイヤーに属し、モジュール間の依存関係は必ずレイヤー間の依存関係に従います。
レイヤーは、現在のところ4つに分けています。もっとも中心的な存在がDomainで、その名の通りABEMAの事業やサービスとしての知識をドメインモデルとして実装する部分です。このレイヤーはおおよそClean ArchitectureのEntitiesに対応しますが、後述するドメイン駆動設計を意識してDomainという名前に変更しています。
Domainに実装されたドメインモデルを使って、アプリの様々な機能仕様を実現するのがUseCaseです。Domainが事業全体で共通する知識であるのに対して、UseCaseは同じサービスでもアプリの機能が異なれば変わってくる部分です。AndroidやiOSといったモバイルアプリと、ブラウザ向けに提供されているWebアプリやABEMAボタン対応テレビなどに搭載されたABEMAアプリとでは、提供する体験や想定している使われ方が異なります。そういった差によって、このUseCase部分に実装されるロジックが変わってくると考えています。
その他、ドメインオブジェクトの永続化を担うRepositoryの実装などが置かれるDataレイヤー(図中ではRepositoryと表記している部分)と、システムの入出力をUseCaseに対応付ける画面実装やViewModelなどが置かれるUIレイヤーを置いています。Dataレイヤーに置かれるRepositoryはドメインオブジェクトが単位となるため、そのインターフェースはDomainレイヤーが定義しています。
モジュールの定義をプラットフォームごととしているのは、それぞれの技術によってモジュールに対応する概念(AndroidにおけるパッケージやiOSにおけるフレームワークなど)が異なるので、それぞれ最適な分け方が異なると考えたためです。また、例えばUIレイヤーの中の最適なモジュール構成は、同じiOSでもUIKitを用いるかSwiftUIを用いるかによって変わるかもしれません。しかし、レイヤーで定義されたアーキテクチャのルールは、UIの実装技術によって左右されることはありません。ただ、プラットフォームごとの柔軟性を持たせているとはいえ、実際のところ本稿執筆時点ではAndroidもiOSもほぼ同様の分け方になっています。
なお、iOSにおける具体的なモジュール構成や実装イメージについては、以前チームメンバーの廣川が弊社イベントにて発表した資料をご参照ください。こちらの登壇を行ったエンジニアをはじめとして、リアーキテクチャを直接的に担当したエンジニア以外のメンバーも、積極的に議論や情報発信を通じてリアーキテクチャの取り組みに貢献してくれています。
さて、このような整理により、いわゆるドメインロジックやビジネスロジックをどこに記述するべきかをアーキテクチャとして明文化することができました。しかし、今回のリアーキテクチャでこそClean Architectureを土台として議論を開始したものの、実際のところアプリ開発で用いられるアーキテクチャとして紹介される例のほとんどが、ドメインロジックやビジネスロジックをどこに実装すべきか自体は明示していると考えられます。Clean Architectureのような区別はないものの、MVCではModelが “専門知識を表現したり、アプリケーションのデータやそれを扱うロジックを定義する” 部分であると説明されています (1) し、もともと採用したFluxでも “Storeがビジネスロジックを担う” と明確に説明されています (2) 。では、なぜこれまでの実装では前述の “ビジネスロジックが明確に分離されていない” という課題が生まれてしまったのでしょうか。
リアーキテクチャを担当するAndroid・iOSの両エンジニアで具体的な実装や移行の道筋を議論していく中で、そもそもの “ビジネスロジックかそれ以外か” のような分類や、Clean Architectureで言及される “企業全体の” ロジックなのか “アプリケーション固有の” ロジックなのかといった区別は、決してメンバー全員が同じ判断を下せるようなものではないことに気付かされました。
現実のアプリは様々な機能と、それに伴う複雑な仕様を持っています。一件単純そうな機能を持った画面だったとしても、動画再生やUIアニメーション、サービス改善のためのログマネジメント、ABテストや様々なAPIとの並行した通信など、多くの工夫の上に成り立っているものです。そうした機能の実装は、UIからサービスとしての知識までを横断して扱う必要があり、それらを分類して捉え直す作業は容易ではありませんでした。
すなわち、多くの要素を含んだ機能仕様の中から、サービスとしての知識やアプリケーションとして満たすべき仕様とそれ以外を明確に分離するプロセスが曖昧なまま、それらが継続的に分離された状態を保つことができるアーキテクチャであるとは言えない、と考えるに至りました。その結果、仕様を整理・分析しながら実装まで落とし込んでいくプロセスを取り入れていくための取り組みへとつながっていきます。
仕様に基づいて実装するには
リアーキテクチャの議論と並行して、仕様から実装を導いていくプロセスを取り入れていくために、主に2つの取り組みを開始しました。ひとつはドメイン駆動設計を参考にしたドメインモデリング、もうひとつはICONIXプロセスを参考にした実装プロセスの形式化です。それぞれの取り組みについてご紹介します。
私たちがベースとしたClean ArchitectureにおけるEntity、あるいはMVCにおけるModelも、業務やサービスの知識を表現するのが役割とされています。そして、こうした知識は決してエンジニアが技術的な都合のみで検討できるものではなく、職種を問わず実際に業務を行ったりサービスの知識を持っている人(=ドメインエキスパート)と議論しながら作り上げていかなければならないものだ、と主張している設計手法こそ『ドメイン駆動設計』であると考えています。
ABEMAでも、普段仕様書やテキスト・口頭でのコミュニケーションで使われる業務やサービスの概念・知識・用語を分析して整理する作業が、リアーキテクチャの文脈に限らず組織全体の業務効率化や課題解決につながると考えました。そこで発足したのが、職種を横断した取り組みとしての「ABEMA用語集」プロジェクトです。これまで曖昧さのある慣用的な名前で呼ばれていた概念や新規に追加される機能などを対象として、クライアントエンジニアを中心としつつデザイナーやPMにも参加してもらいながら、UIの意図や事業的な背景を踏まえつつ用語を議論して整理しています。なるべく前提知識を必要とせず、職種を問わずに誰でも参加できる取り組みとして位置付けるために、あえて “用語集” という親しみやすさを持った形としました。
現段階では開発メンバーを中心としていますが、運用や広告、宣伝やコンテンツの管理など分掌が多岐にわたるABEMAにおける “ユビキタス言語” の構築を目指して、もしくはどういった範囲であればユビキタス言語が構築できるのかを見極めるために、この取り組みを広げていきたいと考えています。
なお、この用語集は、ABEMAのデザインシステムである「conte」を作るプロジェクトの一環として取り組んでいます。conteの取り組みやビジョンは、以下の資料で詳しく説明されています。
conte – ABEMA’s Design System from Yusuke Goto
もうひとつの取り組みは、「ICONIXプロセス」と呼ばれる形式化された手法を開発に取り入れることです。ICONIXプロセスは『ユースケース駆動開発実践ガイド』 という書籍で詳細に解説されている、事業的な要求を列挙した機能要求を分析してドメインモデリングを行うところから始まり、シーケンス図やクラス図といった実装に近い詳細な設計まで段階的に落とし込んでいくプロセスです。このプロセスを、Nativeチームのエンジニアで実際に新機能の開発に導入し、そのメリットやデメリットを評価する取り組みを行いました。プロセス自体の詳細な説明については書籍や他の記事に譲り、ここでは私たちのチームで実際に作成したアウトプットをご紹介します。
私たちのチームでは、ドメインモデリングやユースケース図の作成、ロバストネス分析といったグラフィカルな面が強い作業をMiroで、シーケンス図やクラス図の作成をPlantUMLの埋め込みに対応したesaで行いました。
その後実際にコードとして実装する作業を経て、プロセスを一巡したところでKPT形式での振り返りを行いました。目立ったものを一部抜粋すると、以下のような評価や意見が上がりました。
よかったところ
- これまで属人化していた、経験を積んだエンジニアが無意識にやっているようなプロセスを体系化して進められ、ドキュメントも残せた
- 複数人で要件や仕様の認識を合わせながら進められた
- 仕様に対する懸念にも事前に気づくことができた
- QA項目まで作れて、実用的なアウトプットが出せた
- 実装イメージが沸いた状態で実装に取り掛かれた
- 時間はかかるが、それに対するメリットは十分に得られるかもしれない
課題に感じたところ
- チームの誰でもできる状態にするまでは時間や学習コストがかかる
- 書籍のみで完全にプロセスを理解できるとは言えず、実践してみると分からない点も多い
- 大元となる機能要求自体が確定していない段階で作業を始めると、着手できない部分や手戻りが大きい
- ステップごとに適したツール(Miroやesaなど)が異なるため、情報の整理が難しい
ICONIXプロセスにおいてもドメインモデリングが最初のステップとなっていることからも察せられる通り、前述したドメイン駆動設計の考え方とも密接な関係を持つものです。最終的な目標は、このICONIXプロセス自体をそのままチームのプロセスとしてルール化することではなく、用語集やリアーキテクチャの取り組みとも掛け合わせながらエッセンスを柔軟に取り入れていくことです。その結果として私たちのチームに適した、言わば “ABEMAプロセス” を見出していきたいと考えています。
これからの取り組み
最後に、リアーキテクチャを契機として広がっていった様々な発展的な取り組みについてご紹介します。
ひとつは、実装の共通化です。リアーキテクチャにより、各プラットフォームの実装の中で何が共通して何が異なるのかを整理して捉えることができるようになりました。Domainレイヤーに置かれるドメインモデルは事業・サービスとして共通する知識のため、理想的には全てのプラットフォームの実装で共通のものとなるはずです。UseCaseは先述したとおりアプリの機能によって変わる部分なので、Webやテレビとは異なるものになる可能性が高い一方、AndroidとiOSは多くが共通するべきと考えられます。
こうした中で、実際にコードレベルで実装を共通化するための技術として、Kotlin Multiplatformを検証しています。とはいえ、DomainやUseCaseを一気に置き換えていくのは難しいため、現状では技術自体の検証段階としてAPIクライアント部分から試験的に導入しています。また、現状ではAndroidとiOSのみが共通化されていますが、技術的にはWebなどの他プラットフォームにも対応できる可能性があるため、チームを横断した議論・技術検証が行われています。
Kotlin Multiplatformの取り組みについては、弊社エンジニアが発表した以下の資料でも詳しく説明されています。
もうひとつの取り組みである「テストポリシー」についてもご紹介します。リアーキテクチャでレイヤーやモジュールといった各コンポーネントの役割が明確に定義されたことで、これまで各エンジニアが経験的に行っていたユニットテストにより担保できる品質の範囲を明確にすることができました。それをきっかけとして、自動・手動問わず様々な種類が存在するソフトウェアテストについて、それぞれの役割や性質を整理して明文化し、テストポリシーとすることで、各テストを組み合わせて担保できるアプリの品質を最大化できるのではないかと考えています。
今のところ、Nativeチームとしてのテストポリシーを作成し、さらに細かいテスト種別ごとのポリシーとしてまずユニットテストポリシーを作成しています。その上で、これまで積極的には行えていなかった結合テストや自動化されたE2Eテストなどについてのポリシーも作成していきたいと考えています。
これらのテストポリシーも皆様に共有できるようなものを目指して議論を続けていますが、本稿では一旦、テストポリシーの背景部分を抜粋してご紹介します(一部公開のため修正しています)。
高品質なアプリをリリースし続けるためには、これからリリースするアプリが正しく動作することをリリース前に確認する必要がある。しかし、操作手順、課金状態、端末やOSの種類など、アプリやユーザー、動作環境の状態の組み合わせは無数に存在するため、アプリをリリースするたびに受け入れテストで全ての機能が正しく動作することを確認すると、膨大な時間と人的リソースが必要となる。そのため、ABEMAにおいて受け入れテストは一連の主要な機能のみに絞ってテストしている。
受け入れテストを主要な機能に絞ることにより、高頻度のリリースのサイクルを実現することを可能にしているが、その他の修正で意図しない挙動の変化(リグレッション)があったとしても、受け入れテストで検知することができない。受け入れテストで検知できない機能は、その他のテストで検知できることが望ましい。
ユニットテストや結合テスト、UI Automationテスト、Visual Regressionテストなど、テスト手法は数多くある。それぞれのテストには特性があり、相性が良い領域が異なる。個人やチームによってテストの選択判断が異なると、アプリ全体で不必要または非効率なテストを行ってしまう懸念がある。
そのため、各テストの相性と開発フローに合わせたテストポリシーを定めることにより、アプリ全体で最も効率の良いテスト計画を実現したい。アプリの品質を高いまま維持するためには、ユニットテスト、結合テスト、E2Eテストおよび受け入れテストを、それぞれのトレードオフを考慮しつつバランスよく組み合わせることで、担保できる品質を最大化することが求められる。
最後の取り組みとして、アーキテクチャについて継続して行われている議論についてご紹介します。今回のリアーキテクチャでは、アプリにドメインモデルを中心とした “同心円” を設け、APIから受け取ったデータを一度ドメインモデルに変換した後にUseCaseでの処理を経て表示用のデータに変換することとしてます。一方で、APIが返却するレスポンスは必ずしもドメインモデルに対応するとは限らず、表示用に最適化されたデータを返す場合や、ドメインモデルに対して過不足のあるレスポンスである場合があります。そういった場合、アプリ側で非効率なデータ変換が必要になったり、アプリのドメインモデルが “サービスで共通のモデル” としては違和感のあるものになったりしてしまいます。
こうした課題は、ドメインモデルを中心とした “同心円” がクライアントアプリとサーバーサイドでそれぞれ存在するものなのか、もしくはクライアントアプリがサーバーサイドの “プレゼンテーション層” としての役割しか持っていないのか、という設計上の選択次第で生じるものであると考えています。そしてこの選択はどちらかが優れていると言えるものではなく、それぞれの開発スピードやチームのリソース、組織の構造に応じて設計していく必要性を感じています。
アプリのアーキテクチャを検討する際、どうしてもAPIの向こう側は “外側” として踏み込まず、アプリに完結した議論に終始してしまいがちですが、サービスを実現するシステム全体の設計を踏まえずには検討できないものであると考えています。この点はサーバーサイドを担当するエンジニアも交えて、引き続きチームで議論を続けています。
まとめ
ここまで、ABEMAのNativeチームで行われてきたリアーキテクチャの議論についてご紹介してきました。また、もともとは技術的な課題を解決するためのリアーキテクチャが、組織全体を巻き込んだプロジェクトを含めた様々な挑戦へと繋がっていることをご紹介してきました。
こうした取り組みから、理想的なアプリの設計や実装は、必ずしも技術のみから導けるものではないということに気付かされました。私たちが作っているサービス自体に向き合い、それをどんなチームで、どんなプロセスで作り上げていくのかを考慮することが、優れたアプリの設計や実装を導く鍵であると考えています。
これらの取り組みはいずれも始まったばかりであり、そして長期的に続けていかなければならないものです。今後の発展に伴って、引き続きDevelopers Blogや各勉強会などでの発信を続けていきます。また、弊社ではサービスやチームと向き合いながら技術に挑戦するエンジニアを募集しています。興味を持たれた方はぜひインターンなどへのご応募や、Twitterでのご連絡を頂ければ幸いです。共に議論できることを楽しみにしています。