
オンライン時代を経たネイティブアプリのデータフロー(後半)
こんにちは。ネイティブエンジニアのszです。
以前投稿した記事の後半になります。
改めて、この記事でいうデータフローとは、以下の3つのデータをプログラム上でどう扱うか、を指すものとします。
- リソースデータ
- 画像ファイルや音声ファイル
- マスタデータ(ゲームデータ)
- キャラクター名のようなゲームの設定値や、レベルデザイン等で入力するパラメータのように、どのユーザから参照しても同じ値になるデータ
- ユーザデータ(セーブデータ)
- ゲームの進捗情報など、ユーザ毎にパラメータの値が異なるデータ
前半では、なぜデータフローが大切なのかという話の中で、クライアントを開発する上での、目に見えない、置き去りにされやすい部分であるというお話をしました。
後半ではアーキテクチャを考えながらその目に見えない部分を浮き彫りにし、実際の開発でどのようなアーキテクチャで開発してきたかをご紹介します。
「Viewとロジックを分ける」、という言葉はよく語られるのですが、データフローについてはその一歩先、または根幹に位置するものだと考えています。
MVC(Model View Controller)という考え方はアプリ開発者なら聞いたことがある人が多いと思いますが、そこにこのデータフローに関係する考えはでてきません。
DDD(ドメイン駆動設計)などデータを意識した考え方もありますが、今回はよりシンプルな三層アーキテクチャと呼ばれる考え方がわかりやすそうなので、それをベースに説明を進めたいと思います。
- プレゼンテーション層(Presentation Layer)
- UIの表示やユーザ操作等のイベントに関する制御を行います。
- MVCでいうVとかCとかにあたります。
- ドメイン層(Domain Layer)
- アプリケーションに依存するロジックの実装を行います。
- アプリケーション層とかビジネス層とかいう呼び方もします。
- データ層(Data Layer)
- 通信やローカルストレージのデータ管理を行います。
MVCに比べ、データ層があることでだいぶ説明しやすくなったのがポイントです。
これは持論になってしまうのですが、MVCとかDDDとか、型にはめることが大事なのではなく、より良い設計を考える、ことが大事です。
最低限守ることと書いたものの基本的にはクラス設計しっかりしましょうというだけでした。よく言われる疎結合にするか密結合にするかとかいうところですね。
とはいえ、参考になる考え方はあったほうが良いと思うので、オススメ記事を紹介しておきます。
まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて
iOSと書いてありますが、コードなどの実装依存箇所以外の考え方はiOSに限らず参考にできると思います。
さて、前述のように基本的には型にはめずに適宜考えましょう、ということが言いたいのですが、実際に筆者が過去に開発に関わったアプリの構成をご紹介します。
まずは、TwitterやLINEのようなメッセージアプリです。
サーバにある情報を通信で取得して表示するだけのシンプルな構成でした。
ゲームアプリとの比較になればという意図でご紹介します。
- ViewController/View
- UIの表示やイベントの管理を行います。必要に応じてドメイン層から表示に必要なデータを取得します。
また、クライアントにあるリソースデータを直接取得します。
- UIの表示やイベントの管理を行います。必要に応じてドメイン層から表示に必要なデータを取得します。
- DataSource
- クライアント側のロジックが少ないアプリだったので、主にデータをとってくるためのラッパーのような位置づけです。通信してデータを取得・更新したり、キャッシュからデータを取得したりするロジックを書くところです。
- APIManager
- 通信処理や認証、エラーハンドリングなど、通信に関する共通処理を行います。
- Local Data Cache System
- 通信で取得したデータをローカルストレージにキャッシュします。
前半の記事で「サーバとネイティブの双方にデータがあると複雑になる」と説明しましたが、このアプリの場合は基本的にサーバの情報を表示するだけなので、シンプルな構成でも混乱なく開発できた例になります。
次に、通信はあるものの、ライトゲームと呼ばれる類のアプリです。
例えばポーカーやソリティアのようなミニゲームをひたすらやり続ける、といった形式のゲームです。
メッセージアプリとは違って、基本的にユーザデータはクライアント側にあります。
また、今回のアーキテクチャ上では出てきませんが、同時に3アプリ並行で開発し、基盤となる共通実装部分(データ層+α)とアプリ固有実装部分とを明確に分けて開発したプロジェクトでした。
メッセージアプリとの差分のみ説明します。
- Model
- 正直担当してなかったので明確にモデルにあたる実装があったか覚えてないのですが、アプリ依存のロジックを書くところです。
- OnMemory Cache Data / LocalData Manager
- アプリ起動中のみキャッシュするデータと、ローカルストレージに保存するデータを管理します。
ちなみにDatabaseといっても、大規模で大量なデータを扱うわけではなかったので、SQLiteなどのデータベースを使っていたわけではなく、テキストベース&暗号化という形で保存していました。UnityでいえばPlayerPrefs、Cococs2d-xでいえばUserDefaultのような形式です。
次に、弊社リリース中のサービスである「グリモア~私立グリモワール魔法学園~ 」についてです。
ソーシャル要素が多く、比較的大規模なカードゲームです。
詳細は是非ダウンロードして遊んでみてください。
公式HP:http://grimoire.applibot.co.jp/preregister/grimoire_new/
公式Twitter:https://twitter.com/Grimoire_Staff
- 比較的規模の大きめの開発
- クライアント側のベースは別プロジェクトで開発されたもの
- Cocos2d-xでの開発
これも差分のみ書きます。
- API Network
- http通信の管理を行います。詳細は新規開発のところで後述します。
- Option Data System
- サーバとの同期が必要ない、クライアントのみで管理するデータを扱います。SQLiteを用いてローカルストレージにデータを保存します。
- Master Data System / User Data System
- サーバの情報をクライアント側に保存し、利用できるようにします。基本的にはDatabaseファイルに都度アクセスしたりせずに、データをメモリ上にキャッシュして扱います。根本の仕組みはOption Data Systemと同じですが、DownloaderやAPINetworkを通じてサーバの最新情報を取ってきます。
- Resource Manager
- クライアントにあるリソースファイルを管理します。例えば、グリモアではファイル名を指定すれば、どのパスにデータがあってもロードできるようにしています。こうすることで、アプリ層はアプリ内のリソースとダウンロードしたリソースとの区別を意識する必要がなくなります。一方で、リソースファイル名は必ずユニークにする必要があります。
- Downloader
- リソースファイルや、マスタ・ユーザデータのダウンロードを行います。
- DataWork
- Modelに必要なデータを取得・管理します。
- Model
- Viewで使用するために最適なモデルクラスを実装する。通信が不要なデータはDataWork経由で取得します。
特徴としては、Model(ドメイン層)が通信を管理する点です。データ層に封じ込めないのは、通信中ということをプレゼンテーション層が把握し、UI表示を行う必要があるためです。
また、リリース当初のグリモアは以下の様な設計になっていました。
細かい点を挙げればキリがないのですが、前半の記事で紹介した不具合の例に関係しそうなところを挙げておきます。
- 基本的にタイトル画面をタップするまでは一切の通信を行わない
- 割り込み処理等によるフローの複雑化を防ぐ、タイトル画面に問い合わせがあるので、
そこに到達するまでに進行不能になり得る実装は避ける
- 割り込み処理等によるフローの複雑化を防ぐ、タイトル画面に問い合わせがあるので、
- アプリ起動からマイページへいくまでの間に、すべてのマスタデータとユーザデータのロードを実行する。
- データが膨らむとロード時間が相応にかかるので、運用によってデータが増えた場合は最初にロードするデータは調整していきます
- データロードが実行されていない場合でも、Master Data System / User Data Systemに要求したデータが未ロードの場合、必ずロードを走らせる
最後に現在開発中のアプリについて紹介します。基本的な構成はグリモアと同じため、各コンポーネントの詳細について説明をしていきます。
※ 基本的な構成はグリモアと同じですが、中の実装はだいぶ異なります。また、新規開発のため、詳細と言いつつ書ききれないことも多いのでその点はご容赦ください。
ちなみにグリモアはCocos2d-xですが新規開発中のアプリはUnityです。
まずはhttp通信についてです。
- HttpClient
- http通信時の共通実装を行うクラスです。実際の通信や通信結果のシリアライズ/デシリアライズはここで行います。Unityなので今はwwwを使用して実装していますが、後々やっぱりHttpWebRequestにしたい、となれば、ここを差し替えるだけで変更が可能、という位置づけのものです。
- API Manager
- 応答待ちの通信の管理とアプリケーションに依存しないAPI通信に関する共通処理を行います。例としては、通信前の割り込み処理のチェック、通信中のインスタンスの把握やキャンセル、接続失敗時のリトライ処理、致命的なエラーが出た場合の全通信のキャンセル、等になります。
- API Common Logic
- http通信前後のアプリケーションに依存する共通処理を行います。例としては、共通リクエストパラメータの生成、通信前の割り込み処理、通信後の共通レスポンスのエラーハンドリング、等になります。
- API Executant
- API通信の実行クラスです。APIごとにクラスを作成し、API固有の処理を行います。例としては、通信処理のラッピング、リクエストパラメータ&レスポンスパラメータの定義、シリアライズ&デシリアライズ機能、等になります。
- API Logic
- 複数APIを実行するようなものなど、Modelが必要とするデータを取得するのに最適なロジックを実装します。グリモア同様、通信中かどうかはこの層で把握できるようにしています。
- Table Data System
- 通信で取得したサーバの情報のDatabaseへの書き込みと読み込みを行います。
次にマスタデータとユーザデータです。
- Binary File System
- ローカルストレージへのファイル保存と読み込み、暗号化を行います。独自のバイナリ形式で保存するのかSQLiteで保存するのか、等はここが吸収する(選択できるようにする)予定です。今のところ独自のバイナリ形式のみ対応しています。
- Table Common Logic
- データの書き込みや保存など、共通処理を行います。
- DatabaseManager
- 読み込んだデータの管理を行います。ローカルストレージにあるデータをメモリ上にキャッシュしたり、一括ですべてのデータのロード/アンロードを行います。
- Table Data System
- 通信で取得したサーバの情報のDatabaseへの書き込みと読み込みを行います。例えば、データベースの1テーブル=1クラスとし、パラメータの定義等テーブル毎の実装を行います。
- DataWork
- 複数テーブルの情報を扱うなど、Modelが必要とするデータを取得するのに最適なロジックを実装します。
サーバとの同期が必要ない、クライアントのみで管理するデータの構成です。
DataWorkから直接Binary File Systemを使うだけで、データをローカルストレージに保存する仕組みとしてはマスタデータやユーザデータと同じになっています。
次にリソースデータです。
グリモアと違って、ダウンロード機能は中に内包する形になっています。
- Download System
- 要求されたリソースをロードします。リソースの最新版がサーバ上にあればサーバからダウンロードを行った後にロードします。UnityなのでAssetBundleを使っています。
- Version Checker
- リソースの最新バージョンを管理します。AssetBundleManifestに相当する独自の管理ファイルを使用し、最新バージョンの有無とアセットの依存関係を管理します。
- Resource Object
- リソースのロード状態を管理します。
- Resource Manager
- リソースの管理を行います。ロード中のアセットの管理や未使用になった場合の解放を行います。
グリモアと違って、ファイル名によるパス検索はなく、パス指定は必須になっています。
その代わりにDownload Systemを内包することで、上の層からは使うリソースがアプリ内にあるのかダウンロードしたものなのかを意識しなくてもよい設計になっています。
AssetBundleまわりは別途記事を書いたので、興味がある方はそちらも見ていただければと思います。
まとめ
グリモアと新規開発アプリの例では、比較的規模の大きいアプリ開発でのアーキテクチャをご紹介しました。
実際に紹介した設計をしたから上手くいったという確証はありませんが、データフローに関して考えるべきことが多い、ということだけでも伝われば幸いです。
最後に、新規開発中のアプリでは
- API仕様書からのAPI実行クラスの自動生成
- (リクエスト&レスポンスパラメータ定義の生成)
- データベース定義ファイルからのテーブル固有クラスの自動生成
を行うことで、サーバとクライアントのパラメータ定義に差異がでないようにしたり、実装の効率化を行っています。
アーキテクチャとは直接の関係はありませんが、その恩恵の一つということは言えると思います。
この記事へのコメントはありません。