こんにちは、普段MERYのAndroidを開発している栗野です。
最近、AndroidアプリMERYでは2.0の開発を行っており、それに伴ってAndroidのアーキテクチャを見直す取り組みを行っております。 その中で新しく採用し、進めているアーキテクチャについて少し話そうと思います。
ことの始まり
Androidは今まで1.5人体制で開発を進めていたのですが(一人はiOSとの兼務)、少し前から新しいAndroidエンジニアがjoinし、2.5人体制で開発をすることになりました。
そしてちょうどその方が、「DDDな人」だったこともあるのと、少し前に職場の「DDDな先輩」から「エリック・エヴァンスのドメイン駆動設計」を借りて、それを読み始めていたことも重なって、新しいAndroidのアーキテクチャはDDDの考え方を取り入れながら進めていこうということになりました。
その取り組みの中で今回はAndroidに「レイヤ化アーキテクチャ」の考え方を取り入れた部分を紹介したいと思います。
これまでのAndroidのアーキテクチャ
JSONとアプリで使うクラスのマッピングに LoganSquare を使っているのですが、このクラスをすべてのViewやロジックが参照している状態になっていました。 クラスは基本的にpublicになっており、すべての箇所から値の変更が可能になっており、どのタイミングで値が変わっているかを追跡するのが難しい状況にありました。
この図だけみると綺麗に見えますが、実際はこのクラスが様々なViewから参照されてしまっており、サーバーサイドAPIの仕様が変わるとそれに伴ってこのクラスを参照しているすべてのViewを変更する必要がありました。さらにこのクラスがJSONの受け皿になりつつ、View側でも使われるためParcelableなクラスになっていました。
@JsonObject(fieldDetectionPolicy = JsonObject.FieldDetectionPolicy.NONPRIVATE_FIELDS) public final class Model implements Parcelable { public int id; public String title; public String description; public String url; // Parcelableの実装などは省略 }
public final class Fragment1 extends Fragment { private Model model; private ImageView imageView; public void hoge() { // いろいろな処理をする // Modelの構造がAPI都合で変わってしまうと、このメソッドの内部も書き換えないといけない model.title = "hoge"; } public void huga() { // いろいろな処理をする // Modelの構造がAPI都合で変わってしまうと、このメソッドの内部も書き換えないといけない imageView.setImageURI(Uri.parse(model.url)); } }
public final class Fragment2 extends Fragment { private Model model; private TextView textView; public void hoge() { // いろいろな処理をする // Modelの構造がAPI都合で変わってしまうと、このメソッドの内部も書き換えないといけない model.description = "hoge"; } public void huga() { // いろいろな処理をする // Modelの構造がAPI都合で変わってしまうと、このメソッドの内部も書き換えないといけない textView.setText(model.title); } }
なぜプロパティをすべてpublicにしてるかというと、lombok 経由でGetterとSetterを生やしている場合、KotlinからこのGetterとSetterを参照するときに、Kotlinを先にコンパイルしてからJavaのコンパイルが走るので、No Method Errorになってしまうからです。
(MERYでは現状JavaとKotlinが共存していまして、適材適所で2つの言語を使い分けています。) (本来であればGetterとSetterを利用したロジックを担当する別クラスが存在するべきではあるのですが、その当時のスケジュール・工数・優先順位などから、このような簡易的な設計方針となっておりました)
上述のクラスはサーバーサイドAPIのJSONと一対一の対応になっています。そしてその構造のままアプリのUI部分で使用しています。 初期フェーズはアプリ自体が小さいのでサーバーサイドAPI側に追従していくのも簡単かもしれません。また、プロパティ名を変えるだけであればIDEに任せるだけです。 しかし、アプリが巨大化し、かつ、そもそものレスポンスの構造が変わってしまうと、1つのサーバーサイドAPIの変更での影響箇所が大きくなってしまい、容易に変更に追随することができなくなってしまいます。さらに、あるViewがAPIコールなども行うなど、アンチパターンである「利口なUI」になってしまったりしていました。
レイヤ化アーキテクチャという考え方
詳しい説明は、「エリックエヴァンズのドメイン駆動設計」の第4章を見てもらうとして、ここで大事なのは、それぞれの層は疎結合になっている点です。この図を参考にしながら、Androidのアーキテクチャを新しくしていきます。
これからのAndroidのアーキテクチャ
まず、現在のクラスを、「サーバーサイドAPIに依存するモデル(ただのJSONの受け皿)=ApiModel」と、「アプリが使いたい形のモデル=AppModel」に分離することをしました。
ApiModelはこれまでと同じようにLoganSquareでJSONからマッピングされるオブジェクトになりますが、このオブジェクトは直接UIレイヤー、アプリケーションレイヤーで使われることがないためParcelableにする必要はなくなります。
ちなみに、 サーバーサイドAPIとの接続部分には、Retrofit2 を導入し、このライブラリを通してHTTPリクエストし、返ってきたレスポンスをApiModelにマッピングしてクライアントに返すようにします。
AppModelはこの返ってきたApiModelと自分で作成したコンバーターによって生成され、アプリケーションレイヤーに返されるようにします。このとき、AppModelはKotlinの data class として定義するようにし、基本的にnullを許可しないかつ不変オブジェクトとして定義するようにします。
data class AppModel( val id: Int, val title: String, val description: String, val url: URI)
ApiModelとAppModelは一対一になるわけではなく、AppModelはサーバーサイドAPIのJSONに影響するようなことが起きないようにします。 さらに、ApiModelとAppModelが分離したことにより、ApiModelも不変オブジェクトとして扱うことが可能です。(LoganSquareでマッピングするため、private・finalにはできないが、パッケージ可視性でSetterを提供しないという点で)
つぎに、AppModelとViewに表示するためのクラスを分離します。Viewには「UI的な都合によって欲しい構造のモデル=ViewModel」が存在します。それをAppModelに押し付けるのではなく、それ用のクラスを作ってしまって、今度はUI的な要件からAppModelが影響を受けないように修正します。
このViewModelは主にAdapterに入れて使ったり、Bindメソッドの引数に渡してそのモデルの情報をUI上に表示したりするのですが、Viewで使われるためParcelableなオブジェクトにしておきます。代わりに、AppModelはUIから直接使われることがないため、Parcelableの実装は行う必要がありません。 さらにこのViewModelはそのプロパティの有無によってUIを変える処理に適応させたり、このViewModelのbooleanなフラグを切り替えてUIを変更するロジックがあったりするので、必要であればnullableなプロパティ(optionalなプロパティ)を持っていたり、可変なオブジェクトになっています。
data class ViewModel( val id: Int, val title: String, val description: String, val url: URI, val nickname: String?, var isActive: Boolean)
こうすることで、サーバーサイドAPI要件からもUI要件からも独立した部分が抽出できました。この部分が将来的にドメインモデルと呼べるようになることを目指して、いろいろなふるまいを定義していき、「ドメイン貧血症」にならないようにしていこうと思っています。
おわりに
今回、Androidのこれからのアーキテクチャにレイヤ化アーキテクチャの考えを取り入れていく部分を紹介させていただきました。
一般的に、「DDDは導入コストが高い」と言われていますが、1つ1つの手法(今回話したレイヤ化アーキテクチャや、エンティティなど)を抜粋すれば、このように局所的にでも導入していくことは十分可能だと思います。
まだ考え切れていない部分があったり、DDDの理解が及ばないところがあったりするので、現状はこんな感じで進めているんだくらいに思って読んでいただければ幸いです。 これからどんどんDDDを勉強してよりよい設計にしていければと考えています。