Edited at

Androidアプリの設計 ~My Best Practice~

これは食べログAdventCalendar2018 12日目の記事です。

本日は私が今現在これがベストと思っている設計の話をしたいと思います。


はじめに

これはあくまで私の個人的な考えでかつ今現在の話ですので、1ヶ月後には違うことを言っているかもしれませんw

今回の話は以下のような前提を想定しています。


  • 開発メンバーが多い

  • 長期的な運用をする

  • 機能も多数(参照系、更新系など様々)

具体的な数字に関してはメンバーのスキルや、サービスの内容にもよりますので、なんとなくでイメージしてもらえればと思います。

自分の感覚値で言うと、機能に関しては主力な機能が3、4つあれば十分大きいと思います。

上記のような条件にマッチしない場合は、過設計となりコストが高くなるだけになる可能性もありますので、その辺の判断には注意が必要です。

また、この記事の中ではDDD(ドメイン駆動設計)で使用される用語もでてきますが、DDDの考え方などを取り入れているという程度で、詳細に説明するような内容ではないのでご注意ください。

話の流れとしては以下のような形で進めていきます。


  • 全体のクラス図

  • アプリケーションアーキテクチャ

  • GUIアーキテクチャ

  • アプリケーションのモジュール構成

全体的に文が多く、長文のため読みにくくなっているかもしれませんが、どうか最後までお付き合いください!


Application Class Diagram

いきなり概ねの答えからいきます。

class_diagram.png

うん、クラス・インターフェイスの数が多いですね。

それではこのクラス図について順番に説明していきたいと思います。


Application Architecture

アプリケーション全体に関するアーキテクチャについてです。

私の中でのベストは今の所、オニオンアーキテクチャやClean Architectureのような円形のアーキテクチャが良いと考えています。

特に各レイヤーの命名が一番しっくりきたのはオニオンアーキテクチャになります。

一番根幹となるDomainレイヤーを中心に据えているのも、ビジネスロジックの重要さがイメージしやすいですし、Infraレイヤー(Dataレイヤー)が外側にあるという部分も自分のアプリケーションから見たときの外部(クラウドなど)につながっていく感じがするのでわかりやすいと思います。(個人的見解です)

とはいえ、レイヤードアーキテクチャがダメという話ではありません。

要は、Domainレイヤーがしっかりと他のレイヤーに依存しない形ができていて、依存関係の方向が内側に向いていれば良いと思います。

この依存関係の方向がポイントで、ここがしっかり守られていると安定します。

図にするとこんな感じです。よく見かけるやつですね。(Domain ServiceはDomainに含んでしまっています)

architecture.png

まず簡単に各レイヤーの役割を紹介します。


Domain Layer

Eric EvansさんのDDD本によると以下のように記述されています。


ビジネスの概念と、ビジネスが置かれた状況に関する情報、およびビジネスルールを表す責務を負う。ビジネスの状況を反映する状態はここで制御され使用されるが、これを格納するという技術的な詳細は、インフラストラクチャに委譲される。この層がビジネスソフトウェアの核心である


主にはビジネスルール(業務、データの整合性)を担保するのが責務になります。

技術的な部分(DBのCRUDなど)は後述するInfraレイヤーに委譲されます。


Use Case Layer (Application Service Layer)

UseCaseレイヤーは、ビジネスロジック(Domainレイヤー)を利用してユースケースを組み立てるレイヤーになります。

このレイヤーはビジネスロジックそのものではなく、あくまでDomainレイヤーの作業を組み立てて進捗管理するレイヤーになります。

簡単に言うとユースケース毎のシステムの調整役といったところでしょうか。

もし、Domainレイヤーとの違いがわからない場合は以下のブログが大変参考になりました。


Infra Layer (Data Layer)

こちらは一般的な技術的な機能を提供するレイヤーになります。

主にはデータのCRUDなどがそれに相当します。

データのCRUDを提供する際にはDomainレイヤーがどこからデータを取得するのか(キャッシュポリシーも含め)など意識しなくて済むように、Repositoryパターンなどで抽象化することをお勧めします。

注意点としては、Domainレイヤーで定義されたインターフェースを実装するというところです。

依存関係逆転の原則(DIP)を使用して、実行時には依存性注入(DI)をするだけで、Domainレイヤーは独立性が高くなり、たとえInfraレイヤーのDBを別のものに変更しても影響しないようになります。


UI Layer

UIレイヤーはさらに以下の二つに分けられます。


View Layer

ViewレイヤーはそのままViewのことです。

もう少し言うと、UIを構築するためにSDKに依存したクラス等が含まれます。

具体的には以下のようなクラスが存在します



  • Activity / Fragment / View


  • AdpterなどViewを表現するのに使用するクラス

クラス図にはFragmentActivityが記載されていますが、これは一例なので、レイアウトの構成によっては変化します。


Presentation Layer

Presentation レイヤーは以下のような責務を担います


  • View レイヤーからのイベントを受け取り適切に処理する


    • Use Case に問い合わせたり



  • 各イベントの結果をViewに通知する

UIレイヤーに関しての詳細はGUI Architectureで触れますので、一旦ここでは割愛します。


Service Layer

各モジュールの機能を利用する場合はこのServiceを利用します。

イメージとしてはAPIに近いでしょうか。

詳細はモジュール構成で記載しますのでここでは割愛します。


CQRS (Command Query Responsibility Segregation)

機能の数にもよりますが、機能が多い場合はCQRSを用いて参照系と更新系の機能を分離すると良いと思います。

CQRSとは「コマンド・クエリ責務分離」のことで、Command(更新系)とQuery(参照系)の責務を分離しましょうというものです。

これを導入するメリットとしては以下が挙げられます。


  • Repositoryの肥大化を防げる

  • 参照系と更新系という異なる性質のロジックを分離することでそれぞれに適した形を取れる

まず、一つ目に関してはわかりやすいかと思います。

APIの数が多い場合などは、適切に分離しないとどんどん肥大化していきます。

そして肝心の二つ目ですが、参照系と更新系では性質が異なります。

更新系の処理はデータの整合性を担保するために、重要なビジネスロジックを必要としますが、参照系ではそれが必要ありません。そのためビジネスロジック的なEntityやDomainServiceではなくDTO(Data Transfer Object)としてDomainレイヤーに定義して、それをUI層に渡していきます。

cqrs.png

UIの表現をDTOの構造で表していますので、ある意味参照系のビジネスロジックとも言えるかもしれませんね。

こうした異なる性質のものを適切に分離し、インターフェイス名やクラス名も適切に設定することで、全体の見通しが良くなり、可読性が向上します。

また、Query(参照系)に関してはUseCaseレイヤーの省略も可能ならしても良いとは思います。

私はレイヤーがあったりなかったりするのは逆に混乱を招く気がしたので省略はしていませんが、不要なものはない方が良いとも思うので、ここのあたりは統一されていれば、柔軟にしてよいと思います。


GUI Architecture

続いて、GUIのアーキテクチャについてです。

既存からの移行なのか、新規に作るのかなどによって変わるかもしれませんが、ここではクラス図の通りMVPを採用しています。

MVPではViewとPresenterに関するやり取りをContract(契約)という形でインターフェイスとして定義します。

そうすることで、PresenterとViewの依存関係を抽象化し、PresenterをピュアなKotlin / Javaで書けるような状況にしておけば、テスタビリティが高くなります。おそらくUseCaseなどのMockを実装して、ContractView経由の通知をチェックすることで、Presenterのテストは実装できる状況になっていると思います。

mvp.png

もちろんAAC(Android Architecture Component)を利用したMVVMなどが悪いと言う話ではありません。(むしろ個人的には好きです)

様々な状況があるなかで、他のライブラリに依存しない構成が可能と判断してMVPを選択しました。

また、画面遷移に関してもContractとは別インターフェイス(ScreenTransition)で切り出すことで、いちいちFragmentからActivityを取得したりしなくて済むようにしています。

例としてはActivityで実装していますが、画面の構成によっては親のFragmentで実装するなどにしても良いかと思います。


Module Structure

ここは昨今の状況から、マルチモジュール構成が良いと思います。

マルチモジュールを勉強する際に以下の資料を参考にさせていただきました。

内容が資料と重複しますがご容赦くださいm(_ _)m

分割は以下の観点で分割します。


  • 意味のあるまとまり

  • レイヤー


意味のあるまとまりでの分割

意味のあるまとまりで分割することで以下のようなメリットがあります。


  • 複雑な機能をもったアプリを適切に分割できる

  • サーバ側のマイクロサービス化に合わせた構成(エンドポイント毎とか)が可能になる

  • Androidにおいては、Instant AppsやDynamic Deliveryなどのようなモジュールを分割することが必要な機能に対応ができる

この分割をどのように行うかはサービス毎に異なります。

分割の考え方は様々あるとは思いますが、DDD(ドメイン駆動設計)のコンテキストマッピングなどがヒントになるかもしれません。


レイヤーでの分割

モジュールは以下のように分割すると良いと思います


  • メインモジュール


    • 外部公開


      • UI レイヤー

      • Service レイヤー



    • 外部非公開


      • Presentation レイヤー





  • ユースケースモジュール


    • Use Case レイヤー



  • ドメインモジュール


    • Domain レイヤー



  • インフラモジュール


    • Infra レイヤー



上記のように分割し、ビルド設定で依存関係を強制することが可能です。

意味のあるまとまりでの分割と組み合わせることで、各モジュールでActivityやFragmentを用意して、それを繋ぎ合わせれば複雑な機能をもったUIも表現できます。

また、Serviceレイヤーを提供することで各機能を利用する際の入り口がメインモジュールだけとなり明確になります。


Service

例えば、GitHubなどの機能でリポジトリ操作と認証で分けたとして、リポジトリ作成のAPIを叩く時に認証情報が必要になります。

そう言ったときは認証モジュールのSearviceを利用するようにします。

Service -> UseCase -> Domain と繋がることで、各機能が利用できます。

全体的な構成を図にするとこんな感じです。

multi_module.png


さいごに

ここまで読んでくださりありがとうございます。

正直設計に関しては絶対的な正解はないと思っています。

皆さんもサービスやシステム、環境などさまざまな要因の中、最適と思われるものを模索しているかと思います。

今回の設計についての話も、多くの方の書籍やブログ、カンファレンス・勉強会などでの登壇された方の発表内容を参考にさせていただきました。

この記事も、設計をする上での何かしらのヒントになれば幸いです。

明日の食べログAdventCalendar2018 13日目は @tomity さんより「食べログに肉の希少部位を教えてもらう話」です。

なかなか興味深いタイトルですね。お楽しみに!