- 設計を行う上での考え方について
- レイヤードアーキテクチャについて
- モバイルアプリにおけるプレゼンテーション層
- モバイルアプリにおけるドメイン層
- モバイルアプリにおけるデータ層
良い設計をするためには何を気にすればいい?
慎重に良い設計をするべき、ということに否定する開発者はいないと思います。
JetpackComposeを使う時に気をつけなくてはならないのは、データの扱いです。
JetpackComposeは生まれた背景とその性質上、Viewのコードも全てKotlinで作ることになります。
それ故に、状態(State)を使ったデータの操作の設計がうまくできていないと、Viewのコードと、データ操作のコードが互いに依存してしまい完成した後の保守と運用が難しくなる可能性があります。
各レイヤーの役割、データを扱う方法(アーキテクチャなど)とその必要性を理解して設計を行う必要があります。
設計を考える上で大事なこと
設計の基本的な考え方
アプリケーションを設計するとは、変更に強い構造にするために、責務ごとに分けて依存を整理することです。
具体的には次を行います。
- 機能を役割(レイヤー)ごとに分割する
- 各レイヤーの責任を明確にし、責任外の処理は他のレイヤーに任せる
- 将来の変更に備えて、入れ替えや拡張がしやすい単位にする
ただし、実際に難しいのは「将来どんな変更が起こるか」を完全には予測できない点です。
そのため、予測できる範囲は最初に洗い出しつつ、予測外の変更にも耐えられる設計を目指します。
オーバーエンジニアリングを防ぐには
どんな変更にも対応できるように、最初から細部まで作り込んだ設計を用意するのが正解とは限りません。
なぜなら YAGNI(どうせ必要にならない:You Are not Going to Need It.) の通り、使われない機能を先に作るほど無駄が増えるからです。過剰な作り込みは複雑さを招き、読みづらさや保守コストの増加につながります。
大切なのはリファクタリングです。
変化に応じて、規模に応じて、仕様を変えずに設計を変えていく必要があります。
そのためには、仕様を変えないことを保証するにはテストが必要不可欠です。
つまり、テストをしやすいコードを維持することがオーバーエンジニアリングを防ぐ最も重要な手段です。
既存のパターンを参考にする
設計は自分で一から考えなければならないかというと、そんなことはありません。
大抵の問題は先人が通った道であり、すでにパターン化されています。
既存パターンを学びつつ、その中から適切なパターンを選んでいくのが最初のステップです。
以上が設計を行う上での一般的な考え方です。
次に、アーキテクチャについてお話します。
レイヤードアーキテクチャとは
レイヤードアーキテクチャ
UIのあるシステムは入力処理時に、基本的に次のような処理をおこないます。
- UIが入力を受ける
- 入力をシステムが解釈し、処理を実行
- 実行結果をUIに描画
UIとシステムには明確な役割があります。
こういった一連の処理は、役割のごとに分類することができます。
一般的にアプリケーションを設計するときは、全体をいくつかの役割を持ったレイヤー(層)に分けて考える、ということがよく行われます。
このような設計手法をレイヤードアーキテクチャと言います。
3つのレイヤー
レイヤーの分け方は複数パターンありますが、シンプルなケースは以下になります。
1. プレゼンテーション層
- UIの表示、ユーザ入力の受け付けに責任を持つ
- ビジネスロジックを持たず、画面にのみ責任を持つ
2. ドメイン層
- ドメインロジックを持つ(UI以外の処理)
- 単純なアプリでは存在しないこともある
3. データ層
- データの取得・更新に責任を持つ
- DBやHTTPでのデータ取得・更新など
各層の内部の作り方はさらに細かくパターンがあります。
プレゼンテーション層
構成要素
モバイルアプリは主に三つの要素で構成されています。
- ビュー(View)
- ビューモデル(View Model)
- モデル(Model)
1. ビュー(View)
ビューの役割はUIに関わるものになります。
ユーザーがスクリーンを通して見るものに対する情報、つまり構造・レイアウト・形態のことです。
ビューはアニメーションのようなUIロジックのみに注力し、ビジネスロジックは含みません。
ビューはビューモデルの状態の変化を観察(Observing)します。
2. ビューモデル(View Model)
ビューモデルは、ビューが使うメソッドとフィールド(メンバー変数)の実装や、
ビューに状態(State)の変化を知らせます。
ビューモデルで定義されているメソッドとフィールドが、UIで利用できる機能です。
※この機能と情報をどいう形で見せるのかはビューの役割になります。
3. モデル(Model)
モデルはビジネスロジックとデータが含まるビューでのみ利用する、アプリのドメインモデルのようなものです。
モデル内でアプリで使用するデータと関連の動作を定めます。
プレゼンテーション層の中でこのような設計モデルが展開されることになります。
ビューはビューモデルの存在を知っていますが、ビューモデルはビューを知りません。
ビューモデルはモデルを知っていますが、モデルはビューを知りません。
つまり依存関係は一方の方向を指しています。
このように依存関係の向きを揃えることで、わかりやすく変更容易なアーキテクチャを実現します。
一般的にはビューモデルとモデルは1:N関係になります。
ビューモデルはビューがわかりやすくするように、モデルのデータを加工してビューに提供します。
例えば、ビューから違う二つのモデルを活用したデータが必要な場合、ビューでモデルの値を操作して使うのではなく、ビューモデルから二つのモデルのデータを加工してビューではUIだけ扱うようにします。
その他の構成との比較
MVPとMVCパターン
- View
- UI部品の集合体
- Modelから取得したデータを埋め込んで表示する
- Model
- データとルールを持って、処理して結果を返す
- レイヤードアーキテクチャにおけるドメイン層+データ層
- Presenter
- Viewから呼び出される
- Modelのメソッドを呼び出し、その返り値でViewを更新する
- View
- 省略
- Model
- 省略
- Controller
- ユーザ入力を受け、Modelのメソッドを呼び出してデータを更新する
MVVM, MVC, MVP, の比較表
| 特性 | MVVM | MVP | MVC |
|---|---|---|---|
| 構成要素 | Model, View, ViewModel | Model, View, Presenter | Model, View, Controller |
| 役割分担 | ViewModelがViewとデータ同期し、Modelの更新用メソッドを呼ぶ | Presenterがデータの取得・変換・Viewへの反映を行う | Controllerがユーザ入力を受け取り、Modelを更新、Viewを再描画 |
| メリット | • Viewの責務が薄くテストしやすい • Viewの状態はViewModelで完全に表現されている | • Modelに存在しないデータでPresenterが変換してViewに表示できる • MVCより複雑なUIを作れる | • 構造がシンプルで理解しやすい |
| デメリット | • Databindingに対応したフレームワークが必要 • ViewModelが肥大化しやすい | • Presenterが肥大化しやすい • 関数呼び出しベースなので、Viewの再現・テストが難しい | • Modelのデータ更新に追従してViewも更新する必要がある • Modelとは異なるデータの画面への反映が難しくなる |
| ユースケース | 大規模で複雑なアプリケーションに適している。 特に、UIの状態管理が重要であり、データバインディング(*1)を活用できる場合に有効である。 【使用例】リアルタイムデータの表示や、ユーザーインタラクションが多いアプリで使用されます。 | 中規模から大規模のアプリケーションに適しており、特に複雑なUIロジックを持つ場合に有効である。 プレゼンテーションロジックを分離することで、テストが容易になる。 【使用例】ダッシュボードやデータ表示が多く、ユーザー操作が頻繁なアプリで使用されます。 | 小規模から中規模のアプリケーションに適している。 ユーザー入力を受け取って、シンプルなデータ変換と表示を行う場合に有効である。 【使用例】フォーム入力や基本的なCRUD操作を行うアプリで使用されます。 |
※1 データバインディング
ViewとView Modelのどちらかで値が書き変われば、値が変化するたびにViewとView Model、両方の値が変更されるという性質を指します。
ドメイン層
ドメイン層は、複雑なビジネスロジックや、複数のViewModelで再利用されるビジネスロジックがある際に利用します。
一方、すべてのアプリにこのような要件があるわけではないため、この層は省略される場合があります。
複雑さに対処する場合や再利用性を優先する場合など、必要な場合にのみ使用が推奨される。
一般的に、このレイヤのクラスを「ユースケース」または「インタラクタ」と呼びます。
各ユースケースは 1 つの機能を担うようにします。
複数の画面(ViewModel)から同じルールを使用したい場合は、その処理をまとめて再利用できる単位に切り出すようにします。
「タイムゾーンに合わせてメッセージを出し分ける」というルールが複数のViewModelで必要になった場合、その処理を1つのユースケースとして切り出し、GetTimeZoneUseCase のようなクラスにまとめる、という整理ができます。
データ層について
アプリのデータ層は、データの取得・保存・更新(キャッシュや同期など)に責任を持ちます。
アプリの価値そのものを決めるルール(ビジネスロジック/ドメインルール)はドメイン層に置き、データ層はそれを支える役割になります。
データ層の中でこのような設計モデルが展開されています。
データ層の中の登場人物
データ層は、ざっくり言うと次の2つで構成されます。
- Repository(リポジトリ):アプリの他の層(Domain / Presentation)から見た「データの窓口」
- DataSource(データソース):実際にデータを取りに行く担当(ネットワーク、DB、ファイルなど)
Repository(リポジトリ)
リポジトリは「アプリが扱いたいデータ」を、他の層に対して提供する役割を持ちます。
- データを公開する(取得結果を返す)
- データ取得元の違いを隠す(抽象化する)
- 例:ネットワークから取っているのか、DBキャッシュから取っているのかを呼び出し側に意識させない
また、必要に応じて次のような“データ取り扱いのルール”も担当します。
- キャッシュの方針
- リトライ
- 同期
- 複数データソースの結果の統合
- 競合解決
- データの変更の一元管理
DataSource(データソース)
データソースは「1つのデータ取得元」に責任を持ちます。
- 例:ネットワークAPI、ローカルDB、ファイル など
- アプリとシステム(DBやネットワーク)の橋渡しをする
モバイルアプリにおけるデータ層(具体例)
アプリで扱うデータの種類ごとにRepositoryを用意します。
- 例:MoviesRepository(映画データ用)、PaymentsRepository(支払いデータ用)
Repositoryの中で、必要なDataSource(Network / DBなど)を組み合わせてデータを提供します。
参考
このレイヤについて詳しくは、データレイヤのページをご覧ください。
- 設計を行う上では手入れしやすいことが大事で、既存のパターンを活用する
- モバイルアプリのプレゼンテーション層では、モデル / ビュー / ビューモデル (MVVM) や、MVPやMVCといったアプリ設計が存在する
- 構成によって特徴があるので、ケースバイケースで構成を検討する