Kenapa Immutability Itu Penting (Javascript)

Jun 30, 2019 19:56 ยท 1166 words ยท 6 minute read #javascript #programming

Kenapa Immutability Itu Penting (Javascript)
Image by Monsterkoi from Pixabay

Beberapa hari yang lalu PO kami menemukan bug yang cukup unik di salah satu projek legacy kami dimana setelah user meng-upload foto profile-nya, foto tersebut akan tampil dan langsung menghilang sepersekian detik kemudian. Kayak baru kenal tapi langsung diputusin gitu ๐Ÿ˜• Saya pun melakukan Pair Programming dengan temen satu team selama kurang lebih setengah jam. Dataflow-nya oke, redux action gak ada yang masalah, payload dari/ke server pun fine-fine saja. Hmm. Sampai momen dimana kami menemukan satu baris kode yang kelihatannya oke tapi gak oke.

const newState = { ...state }
newState.isRegistered = true
delete newState.profile.picture // <- This guy!

Langsung aja kami misuh-misuh di tempat, “This is such a ridiculous bug! ๐Ÿ’ฉ๐Ÿ’ฉ๐Ÿ’ฉ๐Ÿ˜ก๐Ÿคฌ๐Ÿคฌ๐Ÿคฌ”

Const dan Let

Sebelum memahami kenapa kami bisa menyimpulkan code di atas adalah biang masalahnya, saya mau mengulas dulu apa sih Immutability itu. Immutability dalam programming adalah suatu value yang tidak bisa diubah ketika sudah dideklarasikan. Perhatikan potongan code berikut

const name = "Jihad"
name = "Dzikri"

Javascript akan complain bahwa variable name tidak dapat diubah: Uncaught TypeError: Assignment to constant variable. Mirip-mirip begitu lah. Selama saya bekerja dengan Javascript tiga tahun belakangan, saya hampir-hampir tidak pernah menggunakan let dan lebih memilih const. Sekedar menghindari mutability.

let i = 9

console.log(i + 1 === i + 1)
console.log(i++ === i++)

Kira-kira apa jawaban log yang pertama dan apa jawaban log yang kedua?

Log yang pertama akan bernilai true karena keduanya bernilai 10 console.log(10 === 10). Operasi ini bersifat immutable karena tidak ada variable yang diubah ketika runtime.

Tapi log yang kedua akan bernilai false karena operasi yang satu ini bersifat mutable: nilai i berubah-ubah. Ketika Javascript menjalankan i++ pertama, nilai i berubah menjadi 10.

console.log(10 === i++)

Setelahnya, nilai i akan berubah lagi menjadi 11 disebabkan oleh statement i++ yang kedua.

console.log(10 === 11) // FALSE!

Sampe sejauh ini kita paham bahwa const bisa digunakan ketika kita ingin variable tersebut tidak bisa diganti, dan let bisa digunakan ketika ada variable yang ingin diganti over time.

Object di Javascript

Flat Object

Gak selamanya variable yang dideklarasikan menggunakan const itu nggak bisa berubah. Iya, Javascript ini emang rada-rada gaes. Contohnya gimana, Mas Jihad?

const user = {
  firstName: 'Jihad'
}

// error! Uncaught TypeError: Assignment to constant variable
user = {
  firstName: 'Dzikri'
}

// gak error
user.firstName = 'Dzikri'

Bisa jadi fatal sekali kalau kita nggak aware sama behaviour ini. Gak jarang saya temui beberapa junior developer atau bahkan sudah bisa dibilang mid-level tapi tetap melakukan mutasi seperti di atas tanpa sadar akan konsekuensinya.

function deleteFirstName(user) {
  user.firstName = undefined
  return user
}

const jihad = {
  firstName: 'Jihad',
  lastName: 'Waspada',
  age: 26,
}
const userWithoutName = deleteFirstName(jihad)

console.log('No name:', userWithoutName.firstName)
console.log('Jihad: ', jihad.firstName)
console.log(userWithoutName === jihad)
> No name: undefined
> Jihad: undefined
> true

Kok bisa?? Bukannya yang satu harusnya undefined dan yang satunya tetep 'Jihad'?? Kok dua-duanya undefined??

“Mereka kira mereka bisa menjawab sedangkan mereka termasuk orang-orang yang tidak tahu” - JS 1:12

Keduanya bernilai undefined karena secara default, Object dalam Javascript sifatnya pass by reference, bukan pass by value ketika dilempar ke dalam suatu function/method. Jadi sebenarnya variable jihad dan userWithoutName adalah variable yang sama (point to the same address), hanya namanya saja yang berbeda. Untuk mengakalinya, kita harus ubah sedikit dengan object destructuring atau spread operator.

function solusi1(user) {
  const noFirstName = { ...user }
  noFirstName.firstName = undefined
  return noFirstName
}

// atau

function solusi2(user) {
  return {
    ...user,
    firstName: undefined,
  }
}

// ...

console.log('No name:', userWithoutName.firstName) // undefined
console.log('Jihad: ', jihad.firstName) // 'Jihad'
console.log(userWithoutName === jihad) // false

Now it works..

Saya pribadi lebih suka solusi2 dan menghindari sebisa mungkin solusi1. Dan saya rekomendasikan teman-teman untuk pakai solusi2 sebisa mungkin supaya gak dituduh orang yang tidak bertangungjawab. Hehe canda, biar lebih mudah aja kalau punya nested object ๐Ÿ‘‡๐Ÿป

Nested Object

Prinsip di atas bisa diaplikasikan juga untuk Object di dalam Object. Karena sejatinya operasi { ...user } tidaklah cukup jika object user memiliki object lagi.

 1function removeProfilePicture(user) {
 2  const noPicture = { ...user }
 3  noPicture.profile.picture = undefined
 4
 5  return noPicture
 6}
 7
 8const jihad = {
 9  firstName: 'Jihad',
10  profile: {
11    picture: '/uploads/orang_ganteng.jpg',
12    userName: 'dewey992'
13  }
14}
15
16const userWithoutPicture = removeProfilePicture(jihad)
17
18console.log('no picture: ', userWithoutPicture.profile.picture)
19console.log('jihad: ', jihad.profile.picture)
> no picture: undefined
> jihad: undefined

Keduanya lagi-lagi bernilai undefined karena pada code di baris ke-2 hanya membuat Object baru di level pertama saja. Level berikutnya (profile.picture dan profile.userName) akan tetap menunjuk pada reference sebelumnya. Solusinya adalah dengan membuat object baru lagi!

function solusi3(user) {
  return {
    ...user,
    profile: {
      ...user.profile,
      picture: undefined,
    }
  }
}

Mirip dengan solusi2. Dan alasan inilah kenapa saya lebih suka “style” solusi2 dibandingkan solusi1 karena code-nya lebih straighforward dan terhindar dari any possible bugs yang diakibatkan oleh mutasi object.

Array

Aturan di atas juga berlaku untuk Array karena pada dasarnya Array adalah object ๐Ÿค”

console.log(typeof [])
// > "object"

Dibilang Javascript ini rada-rada. Tapi intinya, diperlukan kehati-hatian juga dalam hal ini.

const persons = [{ age: 23 }, { age: 25 }]

const newPersons = persons.map(person => {
  person.age = 1 // Halo gaes!
  return person
})

console.log(persons)    // [{ age: 1 }, { age: 1 }]
console.log(newPersons) // [{ age: 1 }, { age: 1 }]
console.log(persons === newPersons) // FALSE!

Intermezzo dengan React

Satu kasus yang cukup simple dimana mutability bisa mengakibatkan kita garuk-garuk kepala, mikir keras kenapa component kita gak jalan sesuai yang diharapkan. Mari berasumsi ada sebuah component yang gemuk dan expensive dari segi rerendering sehingga kita perlu mengimplementasikan method shouldComponentUpdate

class ExpensiveComp extends React.Component {
  // ...

  shouldComponentUpdate(nextProps) {
    if (nextProps.user !== this.props.user) return true;
    return false;
  }

  // ...
}

Jika object user diubah dengan cara yang mutable, component tersebut gak akan pernah bisa melakukan rerendering karena object user yang baru dianggap sama dengan yang lama โ‡’ bisa-bisa gak reaktif sama sekali. Immutability dalam hal ini membantu menghilangkan kompleksitas-kompleksitas yang sebenarnya tidak perlu.

Penutup

Sekarang kita sudah cukup paham behaviour Object di Javascript yang memiliki nature pass by reference ๐ŸŽ‰ Ada beberapa keuntungan yang didapat jika menghindari mutasi variable dan object.

  • Sadar atau tidak sadar, ketiga function di atas (solusi1, solusi2, solusi3) semuanya adalah pure function. Yang dimaksud dengan pure function adalah function yang tidak mengubah nilai di luar scope-nya. Ketiga function tersebut tidak mengubah object user, mereka justru mengembalikan object baru.
  • Karena pure function inilah Referential transparency dapat tercapai. Sehingga nggak akan ada ceritanya suatu function ngebuat error bagian aplikasi yang lain yang sama sekali gak ada hubungannya sama function ini. Unknown side effects are always evil.
  • Dan yang paling penting: memudahkan proses debugging! Gak pingin kan dijadiin bahan cacian sama developer lain yang maintain code kita nantinya hanya karena rookie mistake begini.. ๐Ÿคช
  • Di beberapa bahasa yang support multithreading semacam Java atau Scala, Immutability dapat menghindari program dari race condition dan berjalan di thread yang safe

Saran saya pribadi: kalau mutability bisa dihindari, hindari saja. Kalau memang tidak bisa dihindari karena alasan-alasan tertentu, pastikan scope-nya tidak terlalu besar agar kedepannya lebih mudah di-debug.

Semoga bermanfaat ๐Ÿ™‚

Edit on