📗

MVC・MVP・MVVMを簡易コードで理解する

に公開

はじめに

これまでMVCを用いたWebフレームワークを使って開発経験があったのですが、モバイルアプリの開発するためにMVC・MVP・MVVMのアーキテクチャパターンについて詳しく学び始めましたところ学習に躓きました。

アーキテクチャパターンの学習時はMVCパターンとMVPパターンの違いが分からなかったり、MVVMパターンの構造が理解できないことよくあるかと思います。

この記事ではアーキテクチャの概念が理解できるよう最低限のコードで理解できるようにサンプルコード・特徴・メリット・デメリット・処理の例などをまとめました。

サンプルコードはAndroid StudioのKotlinですが、未学者にも理解しやすいようにコメントを設定しています。

① MVC(Model – View – Controller)

Controllerが全体の流れを制御する司令塔となるアーキテクチャがMVCパターンです。
Model(データ)、View(表示)、Controller(制御)の3つに分けます。

MainActivity.kt

//Project名は「kotlintest」としているので、自分のProjectに合わせてください
package com.example.kotlintest

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity

/**
 * 起動部分
 */
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 1. Modelの準備
        val userService = UserService()

        // 2. Controllerの準備(Modelを注入)
        val controller = UserController(userService)

        // 3. Controllerの処理を実行
        //    Controller内部で Model → View を制御する
        controller.showUsers()
    }
}

/**
 * Model(3パターン共通)
 */
data class User(
    val name: String
)

class UserService {

    //nameデータを取得する関数(今回は直接配列を作成)
    fun findAll(): List<User> =
        listOf(
            User("田中"),
            User("佐藤")
        )
}

/**
 * View
 */
object UserView {
    fun render(users: List<User>) {
        users.forEach { user ->
            // ViewはModel(User)の構造を知っている
            Log.d("USER_VIEW", "- ${user.name}") //ModelのUsersからnameを取得
        }
    }
}

/**
 * Controller
 */
class UserController(
    private val userService: UserService
) {
    fun showUsers() {
        // Model(Service)の処理を実行してデータを取得
        val users = userService.findAll()

        // Viewに直接usersを渡して描画を指示
        UserView.render(users)
    }
}

処理の流れ

ユーザー操作

Controller

Model(Service / Repository)

Controller

View(Controllerが直接描画を指示)

MVCパターンでは、ViewがModelの構造を直接参照する点(例: サンプルコードのViewにある${user.name}部分)が特徴であり、ControllerはModelとViewの橋渡しを行います。

Controller部分に役割が集中しやすいため、開発の規模が大きくなるとコードが肥大化しやすい欠点があります(Fat Controller)

ViewがModelの構造を直接参照することがあり、密結合になりやすく改修やテストが他のパターンと比べて難易度が高くなります。

MVCのメリット

  • 構造が単純で開発スピードが速い。
  • MVCはWeb開発のフレームワーク(Ruby on RailsやLaravelなど)でも採用されているアーキテクチャになります。フレームワークに沿って実装により自然とMVCの形式となるため学習コストが低いです。

MVCのデメリット

  • Controllerに処理が集中しやすく、大規模開発になると肥大化したController(Fat Controller)の問題が発生します。
  • ViewとModelが密に結合しやすく、コードの再利用やテストが難しくなります。

※後述のMVPやMVVMは、MVCにおける「Controllerの肥大化」や「ViewとModelの密結合」「テストの困難さ」を解消するために生まれたパターンです。

MVCで行う処理の例

Model

  • DBやAPIからデータを取得する
  • Controllerに渡すデータを表示用に加工する

View

  • 画面の表示
  • Controllerから渡されたデータを画面に表示する

Controller

  • Viewからの入力(ボタン押下など)を受け取る
  • Modelにデータ取得を依頼する
  • Modelから受け取ったデータをViewへ直接渡して表示を指示する
  • 画面遷移を制御する

② MVP(Model – View – Presenter)

Presenter が Viewのインターフェースを通して 画面更新を指示するアーキテクチャ。
MVCから進化して作成されたパターンで、Viewを受け身にしてロジックをPresenterへ移すことができるパターンです。

MainActivity.kt

//Project名は「kotlintest」としているので、自分のProjectに合わせてください
package com.example.kotlintest

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity

/**
 * 起動部分
 */
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Presenterの準備 (ViewとModelを注入)
        // UserActivity(View)の実装をPresenterに渡す
        val presenter = UserPresenter(UserActivity, UserService())

        // 処理開始
        presenter.loadUsers()
    }
}

/**
 * Model(3パターン共通)
 */
data class User(
    val name: String
)

class UserService {

    //nameデータを取得する関数(今回は直接配列を作成)
    fun findAll(): List<User> =
        listOf(
            User("田中"),
            User("佐藤")
        )
}

/**
 * View
 */
//UserActivityのインターフェース
interface UserView {
    fun showUserNames(names: List<String>)
}

//UserActivity
object UserActivity : UserView {
    override fun showUserNames(names: List<String>) {
        //Presenterで設定したnamesを取得(Viewが直接Modelの内容を扱わない)
        names.forEach { name ->
            Log.d("USER_VIEW", "- $name")
        }
    }
}

/**
 * Presenter
 */
class UserPresenter(
    private val view: UserView,
    private val userService: UserService
) {
    fun loadUsers() {
        val users = userService.findAll()
        // PresenterがView専用のデータ(String型)に加工する
        val names = users.map { it.name }
        view.showUserNames(names)
    }
}

処理の流れ

ユーザー操作

View(ボタン押下などを通知)

Presenter

Model(Service / Repository)

Presenter

View(Presenterからの指示で描画)

Presenterが 表示用データを作るため、Viewは Userが疎結合(例:例: サンプルコードのViewにあるname部分でViewはModelのUserを知らない)となり、MVCパターンよりもテストがしやすくなりました。

PresenterからViewを一方的に操作し、Viewは受け取ったデータを表示します。
また、Presenterは Viewのインターフェースを介して操作します。

MVPのメリット

  • ViewとModelが完全に分離されるため、テストがしやすい(Viewでモック用データ表示が可能)
  • Presenterは UIの実装詳細を知らずに、画面状態を論理的に制御できるため、ロジックが明確になる。

MVPのデメリット

  • ViewとPresenterが1対1で対応するため、Presenterの数が増えてコード量が増大する。
  • ViewとPresenterはinterfaceを介して操作されるため疎結合ですが、1対1で対応する構造のためViewの変更でPresenterが壊れることがあります。
  • MVCと同様に大規模開発になるとPresenter が肥大化します
  • ViewとPresenter間のイベント通知や更新処理の手動実装が煩雑。

MVPで行う処理の例

Model

  • DBやAPIからデータを取得する
  • Presenterに渡すデータを表示用に加工する

View

  • UI表示のみ行う
  • Presenterからの指示を受けて、ViewのUIを更新する
  • Presenterへユーザーの操作を通知する

Presenter

  • Viewのイベントを受け取る
  • Modelにデータ取得を依頼する
  • Modelから受け取ったデータをView用に整形する
  • 取得したデータをViewに表示を指示する(Viewは勝手に動かない)

③ MVVM(Model – View – ViewModel)

ViewとViewModelがデータバインディングにより自動反映するパターンです。
MVPパターンと同様にMVCパターンを改良したパターンで、モダンなモバイル・フロントエンド開発の主流です。

MainActivity.kt

//Project名は「kotlintest」としているので、自分のProjectに合わせてください
package com.example.kotlintest

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

/**
 * 起動部分
 */
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // UserActivityを起動
        val intent = Intent(this, UserActivity::class.java)
        startActivity(intent)
    }
}

/**
 * Model(3パターン共通)
 */
data class User(
    val name: String
)

class UserService {

    //nameデータを取得する関数(今回は直接配列を作成)
    fun findAll(): List<User> =
        listOf(
            User("田中"),
            User("佐藤")
        )
}

/**
 * View
 */
class UserActivity : AppCompatActivity() {

    // ViewModelの初期化(簡略化のためFactoryなしの構成)
    private val viewModel: UserViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // データを常に監視する(Observe)
        // データが更新されるたびに処理が実行される
        viewModel.users.observe(this) { userList ->
            userList.forEach { user ->
                Log.d("USER_VIEW", "User Name: ${user.name}")
            }
        }

        // データの読み込み開始
        viewModel.loadUsers()
    }
}

/**
 * ViewModel
 */
class UserViewModel(private val userService: UserService) : ViewModel() {

    // 外部からは変更できない_users(LiveData)
    private val _users = MutableLiveData<List<User>>()

    // Viewが監視する公開プロパティ
    val users: LiveData<List<User>> get() = _users

    // データを取得し、画面表示用の変数に反映する
    //(この変数に値を代入すると、画面側の表示更新処理が自動的に実行されます)
    fun loadUsers() {
        val result = userService.findAll()
        _users.value = result // 値を代入すると、監視側(View)に通知される
    }
}

実行の際はAndroidManifest.xmlに以下のコードを追記してください。

AndroidManifest.xml

///
        </activity>

        <activity android:name=".UserActivity" />
    </application>

処理の流れ

ユーザー操作
 ↓
View
 ↕︎(データバインディング / コマンド)
ViewModel
 ↓↑
Model(Service / Repository)

View側でViewModelの値を監視する仕組み「データバインディング(自動同期)」を利用し、変更の通知があれば表示を更新します。
ViewModelはViewの詳細を知らず、監視対象に設定された変数のみを知っている状態になります。
View側でもViewModelの状態を読むだけとなり、疎結合となります。

MVVMのメリット

  • データバインディングにより、Viewの更新処理を記述するコードが大幅に減る。
  • View と ロジックを完全に切り離すことができ、保守性が向上します。
  • ViewModelがViewに依存しないため、UIを気にせずロジックのみを徹底的にテストできる。

MVVMのデメリット

  • ソースコードを見ての通りMVC・MVPと比べると仕組みが難しくなります。データバインディングの仕組みを理解する必要があり、学習コストが高いです。
  • バインディングのデバッグが難しく、小規模なアプリでは過剰な設計(オーバーエンジニアリング)になりやすい。

MVVMで行う処理の例

Model

  • DBやAPIからデータを取得する
  • Presenterに渡すデータを表示用に加工する

View

  • UI表示のみ行う
  • ViewModelの公開プロパティを監視する
  • プロパティが更新されたらUIが自動で更新される(ViewModelが直接指示しない)
  • ユーザー操作(ボタン押下など)を ViewModel に通知する

ViewModel

  • Viewからイベントを受け取る
  • Modelにデータ取得を依頼する
  • Modelから受け取ったデータをView用に整形する
  • 整形したデータをViewModelのプロパティとして公開
    → Viewはこのプロパティを監視して自動更新

まとめ

MVC (Model-View-Controller)

Controllerが全体の流れを制御する司令塔となるパターン。
ユーザーの入力をControllerが受け取り、Modelを動かし、最終的にViewを切り替えるという全体の制御を担います。Webアプリケーションのフレームワークとして広く普及しています。

MVP (Model-View-Presenter)

Presenterが「Viewのインターフェース」を通して、画面更新を具体的に指示するパターン。
MVCよりもViewを「受け身(受動的)」にします。Viewとロジック(Presenter)を完全に切り離すため、画面がなくてもロジックのテストができるのが大きな利点です。

MVVM (Model-View-ViewModel)

ViewとViewModelが「データバインディング」により、状態を自動同期するパターン。
MVPの進化系とも言えますが、最大の違いは「指示」ではなく「同期(バインディング)」です。ViewModelの値を書き換えるだけで、Viewが自動で更新されるため、モダンなモバイル(iOS/Android)やフロントエンド開発の主流となっています。

終わりに

各アーキテクチャパターンについて、ざっくりとまとめてみました。
アーキテクチャを文字の解説を読んで理解しようとすると難しくなるため、図や実際に自分で処理を作ってみると理解が深まるかもしれません。

また現在のアプリ開発においては、今回取り上げたアーキテクチャパターンからさらに進化したMVI(Model-View-Intent)などが主流となっており、古典的なMVCパターンはシンプルなWeb構造以外では減少傾向にあるようです。

ネットの書き込みによると、規模の大きいアプリ開発の現場でMVPパターンで煩雑だったコードがMVVMパターンにより解決したとの意見を見かけたので、理解を深めることで規模の大きい開発にも対応できるようになると感じました。

関連リンク

https://zenn.dev/1stscratch/articles/64d253147d18ec
Flutterで活用されるアーキテクチャの図や比較表を確認したい際は、是非こちらの記事もご覧ください。

ファースト・スクラッチTech Blog

Discussion