こんにちは。PHONE APPLIでAndroidのアプリ開発を担当している田松と申します。
今回は、スマホチームで現在行っているリファクタリングで、MVPアーキテクチャに沿っていない部分についてどうすべきかを議論した際に、改めて調査したので自分的解釈を書き連ねていこうと思います。
そもそもアーキテクチャとは?
使用される業界やタイミングによって様々な意味を持つみたいですが、ここでいうアーキテクチャは実装を進めるにあたり役割毎にコードファイルを分割し、チーム開発においてどこに何を記述したらいいかを明確化したり、修正における影響範囲を小さくすることを目的として考えるものです。
MVPアーキテクチャについて
MVPとは?
MVPとはModel・View・Presenterのそれぞれの頭文字をとってMVPと呼ばれています。
簡単に役割をそれぞれ記載すると、
めちゃめちゃ簡単に書きましたが、図にすると以下のようにそれぞれの層とやりとりを行います。
Androidの描画処理を行うView層とビジネスロジックを処理するPresenter層に分けることで、Android依存の実装を排除しテストをより書きやすくすることや、役割を明確化することによりどこに何を記述したらいいかを考える手間を減らす目的があります。
※Repositoryというものが登場していますが、Modelはあくまでもデータそのものの定義なのでRepositoryはデータをどう引っ張ってくるかを記述するものだと思ってください。
MVP アーキテクチャを使用した経緯
アーキテクチャには今回使用したMVPの他にMVCやMVVM、Clean Architecture等様々存在しますが、今回他のアーキテクチャとの比較はしませんのでご了承ください。
機会があれば比較や他のアーキテクチャも使ってみたいなとは思っているので、乞うご期待ということで笑
さて、何故今回MVPアーキテクチャを使用したかというと、
- 第一段階のリファクタリングでは構造を大きく変更することはしない
- 処理内容を変えず、ルールに沿っていない部分を修正する
- Baseクラスに依存しているクラスからBaseを剥がす
以上の理由から従来のままMVPで構築することにしました。
MVPアーキテクチャをコードで表現してみる
文字だけでの説明ではイメージが付きにくいと思うので、コードで表現してみようと思います。
DBやAPIとのやり取りを記載するとかなり長くなってしまうので、今回はViewとPresenterがそれぞれの責務をどう分けているのかに焦点を当てて説明していこうと思います。
MVPアーキテクチャを実現するためのやり方は、現場や人によって様々あると思いますが今回は Contract
と呼ばれるインターフェースを用意し、それぞれViewとPresenterで呼ばれる関数を定義していく形で実装していきます。
- Contract
interface SampleContract { interface IView { // 結果の表示 fun showAddResult(result: Int) } interface IPresenter { // 引数二つの数字を足す fun addTwoNumber(num1: Int, num2: Int) } }
Contractインターフェースを用意し、更にその中にViewが実装すべきIViewインターフェースとPresenterが実装すべきIPresenterインターフェースを用意しました。 それぞれ実装すべき処理内容はコメントの通りです。ではそれぞれViewとPresenterにインターフェースを実装していきます。
- View
class SampleActivity : AppCompatActivity(), SampleContract.IView { // インターフェースを実装したものをpresenterとする lateinit var presenter: SampleContract.IPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // presenterを作成 presenter = SamplePresenter(this) // ボタンを押した際の処理 button.setOnClickListener { // 入力内容を取得してpresenterの処理を呼び出す val num1 = editText1.text.toString().toInt() val num2 = editText2.text.toString().toInt() presenter.addTwoNumber(num1, num2) } } override fun showAddResult(result: Int) { // 結果を表示 resultTextView.text = result.toString() } }
- Presenter
class SamplePresenter(private val view: SampleContract.IView): SampleContract.IPresenter { override fun addTwoNumber(num1: Int, num2: Int) { // 足し合わせる処理 val result = num1 + num2 // 描画処理をviewに任せる view.showAddResult(result) } }
サンプルだとわざわざPresenterでやる処理ではないかもしれませんが、規模が大きいチーム開発となってくるとContractにViewで行う処理とPresenterで行う処理がそれぞれ記載されているので、処理の受け渡しが見易くなったり、分割することによってファイルの肥大化及びコンフリクトを防ぐことが出来ます。
また、このようにそれぞれインターフェースを実装させたものを受け取るようにすることで余計なものを渡すことなく処理させたい内容を実装できる点や、Presenter側にAndroid独自の実装を排除することによってテストが書きやすくなるメリットがあります。
PresenterにてSharedPreference等を使用したい場合どうするの?
今回詳しい説明は省きますがRepositoryというものを用意して、そちらに取得を任せることでPresenter側にContext等のAndroid独自の実装を排除することが出来ます。
MVPで実装してみての感想
私的には層も少なく(どう実現するかにもよるかもしれませんが)、Androidにおいて描画はView、ビジネスロジックの処理はPresenterと、どこに記述すればいいか微妙みたいなものが少なく役割もはっきりしているため、学習コストも少なく結構好きです。
ただ、描画とビジネスロジックを分けていることから、一連の処理で複数回ファイルを行き来するパターンもあるので処理が複雑になった場合に追いづらさが若干あるかなと思いました。
また、今回の実装は一例なのと必ずこういう風に実装しなければMVPとは呼ばない!みたいなことはないので、ここから自分たちが開発しやすいように工夫していければと思います。 そしてアーキテクチャについては、学ばなければいけないことがまだまだあるなと私自身感じているので、他のアーキテクチャにも触れて学習していきたいなと思っています。
最後までお読み頂きありがとうございました。MVPとはなんぞやと思っている人の足掛かりになれば幸いです。