MVC / MVVM — Design Patterns

MVC (Model-View-Controller) dan MVVM (Model-View-ViewModel) adalah architectural patterns yang memisahkan kode menjadi bagian-bagian dengan tanggung jawab…

MVC dan MVVM adalah dua cara membagi kode sebuah halaman jadi tiga bagian dengan tugas masing-masing: Model (data dan aturan bisnisnya), View (yang dilihat user), dan bagian ketiga yang menghubungkan keduanya: Controller di MVC, ViewModel di MVVM. Bayangin rumah makan. Dapur menyimpan bahan dan resep (Model), meja makan adalah yang dilihat tamu (View), dan pelayan menerima pesanan, meneruskannya ke dapur, lalu mengantar piring yang cocok ke meja (Controller). MVVM mengganti pelayan yang mondar-mandir dengan papan pesanan yang tersambung: yang ditulis tamu langsung muncul di dapur, dan yang diubah dapur langsung muncul di meja.

🧪 Tebakan cepat: di versi MVVM halaman lacak Kurir Kancil, kode menjalankan vm.resi.set("KCL-0002"). ViewModel vm gak punya satu baris pun yang menyentuh DOM. Apa yang terjadi dengan kotak input resi di halaman? Tebak dulu, jawabannya ada di bagian "Binding dua arah".

Masalahnya: semua dicampur di satu event listener

Halaman lacak versi pertama menaruh semuanya di handler tombol:

document.querySelector("#lacak").addEventListener("click", () => {
  const resi = document.querySelector("#resi").value.trim()
  const parcel = PARCELS[resi]
  document.querySelector("#status").textContent = parcel ? resi + ": " + parcel.status : resi + " tidak ditemukan"
  document.querySelector("#layanan").textContent = parcel ? parcel.service + ", estimasi " + HARI[parcel.service] + " hari" : ""
})

Aturan estimasi tiba tersembunyi di handler klik, jadi aplikasi mobile harus menyalinnya. Aturan itu gak bisa dites tanpa browser. Dan saat status paket berubah dari server, gak ada yang memperbarui halaman, karena kode tampilan cuma jalan saat tombol diklik.

Model: data dan aturannya

Kedua versi memakai Model yang sama. Model gak tahu ada halaman:

const $ = (selector) => document.querySelector(selector)
const HARI = { reguler: 3, kilat: 1, kargo: 5 }

const model = {
  parcels: {
    "KCL-0001": { status: "transit", service: "reguler" },
    "KCL-0002": { status: "diantar", service: "kilat" },
  },
  listeners: [],
  cari(resi) { return this.parcels[resi] ?? null },
  estimasi(parcel) { return parcel.status === "terkirim" ? "sudah sampai" : HARI[parcel.service] + " hari" },
  ubahStatus(resi, status) {
    this.parcels[resi].status = status
    this.listeners.forEach((fn) => fn(resi)) // kabari yang mendengar
  },
  onChange(fn) { this.listeners.push(fn) },
}

MVC: Controller menerima input dan memilih View

// View: cuma menampilkan, gak tahu dari mana datanya
const view = {
  tampilkan(resi, parcel, estimasi) {
    $("#status").textContent = resi + ": " + parcel.status
    $("#layanan").textContent = parcel.service + ", estimasi " + estimasi
  },
  tidakDitemukan(resi) {
    $("#status").textContent = resi + " tidak ditemukan"
    $("#layanan").textContent = ""
  },
}

// Controller: menerima input, bertanya ke Model, memilih tampilan
const controller = {
  aktif: null, // resi yang sedang ditampilkan
  lacak() {
    this.aktif = $("#resi").value.trim()
    this.tampilkan()
  },
  tampilkan() {
    const parcel = model.cari(this.aktif)
    if (parcel) view.tampilkan(this.aktif, parcel, model.estimasi(parcel))
    else view.tidakDitemukan(this.aktif)
  },
}

$("#lacak").addEventListener("click", () => controller.lacak())
model.onChange((resi) => { if (resi === controller.aktif) controller.tampilkan() })

MVC paling sering kamu temui di server: di Laravel, route memanggil controller, controller bertanya ke Eloquent Model, lalu return view("lacak", ["parcel" => $parcel]) memilih Blade View. Express menulisnya res.render("lacak", { parcel }).

MVVM: ViewModel dan binding

MVVM menyimpan state halaman di ViewModel sebagai nilai yang bisa didengar (observable), lalu binding yang menyambungkannya ke DOM. Observable adalah Observer dari lesson 3, untuk satu nilai:

function observable(value) {
  const subs = []
  return {
    get: () => value,
    set(v) {
      if (v === value) return // sama: gak ada yang perlu dikabari
      value = v
      subs.forEach((fn) => fn(v))
    },
    subscribe(fn) { subs.push(fn); fn(value) },
  }
}

// ViewModel: state halaman, gak menyentuh DOM sama sekali
const vm = {
  resi: observable(""),
  status: observable(""),
  layanan: observable(""),
  aktif: null,
  lacak() { vm.aktif = vm.resi.get().trim(); vm.muat() },
  muat() {
    const parcel = model.cari(vm.aktif)
    vm.status.set(parcel ? vm.aktif + ": " + parcel.status : vm.aktif + " tidak ditemukan")
    vm.layanan.set(parcel ? parcel.service + ", estimasi " + model.estimasi(parcel) : "")
  },
}
model.onChange((resi) => { if (resi === vm.aktif) vm.muat() })

// Binding: satu-satunya bagian yang menyentuh DOM
function bindText(selector, obs) {
  obs.subscribe((v) => { $(selector).textContent = v })
}
function bindInput(selector, obs) {
  obs.subscribe((v) => { if ($(selector).value !== v) $(selector).value = v }) // ViewModel ke View
  $(selector).addEventListener("input", (e) => obs.set(e.target.value))         // View ke ViewModel
}
bindInput("#resi", vm.resi)
bindText("#status", vm.status)
bindText("#layanan", vm.layanan)
$("#lacak").addEventListener("click", () => vm.lacak())

ViewModel cuma mengubah nilai. Binding yang menulis ke DOM, dan cuma untuk nilai yang berubah. Saat server mengubah KCL-0001 jadi diantar, MVC menulis ulang #status dan #layanan (2 elemen), sedangkan MVVM cuma menulis #status (1 elemen), karena layanan masih reguler, estimasi 3 hari.

Binding dua arah

bindInput menyambung dua arah. Dari View ke ViewModel: tiap ketikan memicu event input, dan vm.resi ikut berisi teksnya. Dari ViewModel ke View: tiap vm.resi.set(...) menulis kotak input. Jadi vm.resi.set("KCL-0002") membuat kotak input berisi KCL-0002, walaupun ViewModel gak menyentuh DOM. Itu jawaban tebakannya. Di MVC, Controller harus menulis $("#resi").value = "KCL-0002" sendiri.

Inilah keunggulan MVVM di frontend: binding dua arah antara ViewModel dan View mengurangi kode manipulasi DOM yang ditulis tangan. Vue memakainya lewat v-model, Angular lewat [(ngModel)]. React mengikat satu arah (state ke tampilan), jadi perubahan input kembali lewat onChange yang kamu tulis sendiri. Bonusnya, ViewModel bisa dites tanpa browser: vm.resi.set("KCL-0001"), vm.lacak(), lalu vm.status.get() berisi KCL-0001: transit.

Kapan pakai yang mana

⚠️ Jebakan yang sering ketemu

TL;DR: MVC dan MVVM membagi halaman jadi Model (data dan aturan, model.cari, model.estimasi), View (yang dilihat user), dan penghubungnya. Di MVC, Controller menerima input, memprosesnya, dan menentukan View mana yang ditampilkan (tampilkan atau tidakDitemukan), seperti controller Laravel yang return view(...). Di MVVM, ViewModel menyimpan state sebagai observable tanpa menyentuh DOM, dan binding yang menulis ke DOM, cuma untuk nilai yang berubah (update server: MVC 2 elemen, MVVM 1). Binding dua arah (v-model di Vue) membuat vm.resi.set("KCL-0002") mengisi kotak input dan ketikan user mengisi vm.resi, jadi kode manipulasi DOM berkurang. Untuk halaman satu tombol, satu function cukup.

Yang akan kamu pelajari