Blog

IMMUTABLE VS MUTABLE

#Ingénierie

Illustration de IMMUTABLE VS MUTABLE

alt text

Qu'est ce que c'est ?

https://fr.wikipedia.org/wiki/Objet_immuable

Un objet immuable, en programmation orientée objet et fonctionnelle, est un objet dont l'état ne peut pas être modifié après sa création. Ce concept est à contraster avec celui d'objet variable.

Certains types sont de base mutables tandis que d'autres immutables.

Exemple 1 :

let crepesSuperBonnes = {
  name: 'Des crêpes super bonnes',
  oeufs: 3
};

let nbOeufs = crepesSuperBonnes.oeufs;

crepesSuperBonnes.oeufs = 5;

console.log(nbOeufs); // outputs 3

Dans notre mémoire nous aurons donc d'un côté l'objet crepesSuperBonnes et de l'autre un entier nbOeufs.

crepesSuperBonnes               nbOeufs
+-------------------------+--+  +--+
|'Des crêpes super bonnes'| 5|  | 3|
+-------------------------+--+  +--+

Exemple 2 :

let crepesAuRhum = {
  name: 'Des crêpes au rhum',
  oeufs: 3
};

let crepesSuperBonnes = crepesAuRhum;

crepesSuperBonnes.oeufs = 5;

console.log(crepesAuRhum.oeufs); // outputs 5

Dans notre mémoire nous n'aurons donc qu'un seul objet crepesAuRhum car crepesSuperBonnes n'est qu'une référence sur pointeur.

crepesAuRhum
+-------------------------+--+
|'Des crêpes super bonnes'| 5|
+-------------------------+--+

Types immutable

  • Boolean
  • Number
  • String
  • Symbol
  • Null
  • Undefined

Types mutable

  • Object
  • Array
  • Function

String : Cas particulier

let message = 'Hello world';
message[5] = '-';
console.log(message); // outputs 'Hello world'

Cela provoquera une erreur en strict mode et échouera silencieusement en mode non-strict.

Cas concret d'utilisation

Une des choses les plus difficiles à gérer lors de la structuration d'une application est la gestion de ses états. Cela arrive particulièrement lorsque l'application peut exécuter du code asynchrone (comme une requête HTTP ou un input utilisateur).

Exemple :

  • état 1 : l'utilisateur saisi son email/mot de passe et clique sur le bouton "Se connecter"
  • état 2 : l'application regarde si l'utilisateur est autorisé à se connecter
  • état 3 : l'utilisateur est connecté

Le traitement de ce type de comportement à petite échelle peut être gérable, mais cela peut apparaître partout dans une application et peut être un véritable casse-tête, car l'application devient plus grande avec plus d'interactions et une logique plus complexe.

L'immutabilité tente de résoudre ce problème en veillant à ce que tout objet référencé dans une partie du code ne puisse pas être modifié par une autre partie du code, sauf s'ils ont la possibilité de le relier directement.

Solution JavaScript

Certaines nouvelles fonctionnalités ont été ajoutées dans ES6 qui permettent une implémentation plus facile de modèles de données immuables.

Object.assign

Object.assign nous permet de fusionner les propriétés d'un objet dans un autre, en remplaçant les valeurs des propriétés des noms correspondants. Nous pouvons l'utiliser pour copier les valeurs d'un objet sans altérer celui-ci.

let crepesAuRhum = {
  name: 'Des crêpes au rhum',
  oeufs: 3
};

let crepesSuperBonnes = Object.assign({}, crepesAuRhum);

crepesSuperBonnes.oeufs = 5;

console.log(crepesAuRhum.oeufs); // outputs 3

Compatibilité ? polyfills ! http://kangax.github.io/compat-table/es6/#test-Object_static_methods_Object.assign

Object.freeze

Object.freeze nous permet de désactiver la mutation d'objet.

let crepesSuperBonnes = {
  name: 'Des crêpes super bonnes',
  oeufs: 3
};

let crepesAuRhum = Object.freeze(Object.assign({}, crepesSuperBonnes));

crepesAuRhum.oeufs = 5; // fails silently in non-strict mode,
                        // throws error in strict mode

console.log(crepesSuperBonnes.oeufs); // outputs 3
console.log(crepesAuRhum.oeufs); // outputs 3

Immutable.js

https://facebook.github.io/immutable-js/

Immutable.js fournit de nombreuses structures de données immuables persistances, comme par exemple : List, Stack, Map, OrderedMap, Set, OrderedSet et Record.

Ces structures de données sont très efficaces sur les machines virtuelles JavaScript modernes en utilisant le partage structurel en utilisant le partage de structure via des hash maps et des vecteurs tels que Clojure et Scala, ce qui minimise la nécessité de copier ou de mettre en cache des données.

Performances

Les structures de données immuables ont souvent une pénalité de performance en raison des coûts d'allocation de nouvelles données de mémoire et de copie. Considérons ces deux exemples, un qui utilise un tableau mutable et un qui utilise une collection Immutable.js.

Mutable

const list = [];
let val = "";

Immutable.Range(0, 1000000)
  .forEach(function() {
    val += "concatenation";
    list.push(val);
  });

Immutable

const init = {
  list: Immutable.List(),
  val: ""
};

const list = Immutable.Range(0, 1000000)
  .reduce(function(reduced) {
    var next = reduced.val + "concatenation";

    return {
      list: reduced.list.push(next),
      val: next
    };
  }, init).list

Ici, le code immuable sera environ 90% plus lent que le code mutable! Alors que les données immuables peuvent rendre le code beaucoup plus facile à raisonner, il y a un coût associé à cette décision. Comme on peut le voir ici pour concaténation itérative, cela peut avoir un impact majeur sur la convivialité. Heureusement, Immutable.js fournit certaines fonctionnalités où les coûts de performance peuvent être atténués.

let list = list.withMutations(mutableList => {
  let val = "";

  return Immutable.Range(0, 1000000)
    .forEach(() => {
      val += "concatenation";
      mutableList.push(val);
  });
});

Ce constructeur de listes transitoires est toujours plus lent que notre implémentation mutable mais beaucoup plus rapide que notre version totalement immuable.

Pourquoi l'utiliser ?

Performances : éviter les deep-equality

Avec React, c'est utilisé de base dans les React.PureComponent, afin de ne pas re-rerender les components à chaque setState, car un React.Component fait un check sur la deep-equality, ce qui baisse les performances sur les states utilisant des objects complexes de plusieurs niveaux, il compare juste l'égalité de la référence à la place

Aussi utiliser dans Redux, toujours afin de comparer l'égalité sur la référence plutôt que de comparer l'objet entier

Lisibilité : meilleur logique dans le code

const initialArray = [1, 2, 3];
const newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4]

initialArray vaut toujours [1, 2, 3]

Performances : ne pas l'utiliser partout et pour tout

Immutable.JS évite le problème de performances globales en partageant intelligemment les structures de données, ce qui minimise la nécessité de copier des données. Il permet également d'exécuter des chaînes d'opérations complexes sans créer de données intermédiaires clonées inutilement (et coûteuses) qui seront rapidement jetées.

Cependant, si l'application n'utilise pas de state-tree et n'a que peu de data commune, il sera plus performant de ne pas utiliser Immutable.js