Dans la plupart des langages OOP, les objets sont généralement modifiables avec un ensemble limité d'exceptions (comme par exemple les tuples et les chaînes en python). Dans la plupart des langages fonctionnels, les données sont immuables.
Les objets mutables et immuables apportent tous leurs avantages et inconvénients.
Il existe des langages qui essaient de marier les deux concepts comme par ex. scala où vous avez (déclaré explicitement) des données mutables et immuables (veuillez me corriger si je me trompe, ma connaissance de scala est plus que limitée)).
Ma question est: Est-ce que complète (sic!) Immuabilité - aucun objet ne peut muter une fois qu'il a été créé - a un sens dans un OOP contexte?
Existe-t-il des conceptions ou des implémentations d'un tel modèle?
Fondamentalement, l'immuabilité (complète) et OOP opposées ou orthogonales?
Motivation: dans OOP vous utilisez normalement les données on, en modifiant (en mutant) les informations sous-jacentes, en conservant les références entre ces objets. Par exemple, un objet de classe Person
avec un membre father
référençant un autre objet Person
. Si vous changez le nom du père, cela est immédiatement visible par l'objet enfant sans besoin de mise à jour. Étant immuable, vous devez construire de nouveaux objets pour le père et l'enfant. Mais vous auriez beaucoup moins de trucs avec des objets partagés, multi-threading, GIL, etc.
La POO et l'immuabilité sont presque complètement orthogonales l'une à l'autre. Cependant, la programmation impérative et l'immuabilité ne le sont pas.
La POO peut être résumée par deux fonctionnalités principales:
Encapsulation: Je n'accéderai pas directement au contenu des objets, mais communiquerai plutôt via une interface spécifique ("méthodes") avec cet objet. Cette interface peut me cacher des données internes. Techniquement, cela est spécifique à la programmation modulaire plutôt qu'à la POO. L'accès aux données via une interface définie est à peu près équivalent à un type de données abstrait.
Dynamic Dispatch: Lorsque j'appelle une méthode sur un objet, la méthode exécutée sera résolue au moment de l'exécution. (Par exemple, dans la POO basée sur une classe, je peux appeler une méthode size
sur une instance IList
, mais l'appel peut être résolu en une implémentation dans une classe LinkedList
). La répartition dynamique est un moyen de permettre un comportement polymorphe.
L'encapsulation a moins de sens sans mutabilité (il n'y a pas de état interne qui pourrait être corrompu par une ingérence externe), mais elle a toujours tendance à faciliter les abstractions même lorsque tout est immuable.
Un programme impératif se compose d'instructions qui sont exécutées séquentiellement. Une déclaration a des effets secondaires comme la modification de l'état du programme. Avec l'immuabilité, l'état ne peut pas être changé (bien sûr, un état nouvea pourrait être créé). Par conséquent, la programmation impérative est fondamentalement incompatible avec l'immuabilité.
Il arrive maintenant que OOP a toujours été lié à la programmation impérative (Simula est basé sur ALGOL), et tous les langages courants OOP ont des racines impératives (C++, Java, C #,… sont tous enracinés en C). Cela n'implique pas que OOP lui-même serait impératif ou modifiable, cela signifie simplement que l'implémentation de OOP = par ces langages permet la mutabilité.
Remarque, il existe une culture parmi les programmeurs orientés objet où les gens supposent que si vous faites OOP que la plupart de vos objets sera mutable, mais c'est une question distincte de savoir si OOP nécessite la mutabilité . De plus, cette culture semble être lente évoluant vers plus d'immuabilité, en raison de l'exposition des gens à la programmation fonctionnelle.
Scala est une très bonne illustration du fait que la mutabilité n'est pas requise pour l'orientation objet. Alors que Scala prend en charge la mutabilité , son utilisation est déconseillée. Idiomatique Scala est très beaucoup orienté objet et aussi presque entièrement immuable. Il permet principalement la mutabilité pour la compatibilité avec Java, et parce que dans certaines circonstances, les objets immuables sont inefficaces ou compliqués à utiliser.
Comparez un liste Scala et un liste Java , par exemple. La liste immuable de Scala contient toutes les mêmes méthodes d'objet que la liste mutable de Java. Plus, en fait, parce que Java utilise des fonctions statiques pour des opérations comme sort et Scala ajoute des méthodes de style fonctionnel comme map
. Toutes les caractéristiques de la POO - encapsulation, héritage et polymorphisme - sont disponibles sous une forme familière aux programmeurs orientés objet et utilisées de manière appropriée.
La seule différence que vous verrez est que lorsque vous modifiez la liste, vous obtenez un nouvel objet en conséquence. Cela vous oblige souvent à utiliser des modèles de conception différents de ceux que vous utiliseriez avec des objets mutables, mais cela ne vous oblige pas à abandonner OOP tout à fait.
L'immuabilité peut être simulée dans un langage OOP, en exposant uniquement les points d'accès aux objets en tant que méthodes ou propriétés en lecture seule qui ne modifient pas les données. L'immuabilité fonctionne de la même manière dans OOP langues comme dans n'importe quel langage fonctionnel, sauf que certaines fonctionnalités du langage fonctionnel peuvent manquer.
Votre présomption semble être que la mutabilité est une caractéristique essentielle de l'orientation objet. Mais la mutabilité est simplement une propriété d'objets ou de valeurs. L'orientation objet englobe un certain nombre de concepts intrinsèques (encapsulation, polymorphisme, héritage, etc.) qui n'ont rien ou presque rien à voir avec la mutation, et vous en tireriez toujours les avantages, même si vous rendiez tout immuable.
Tous les langages fonctionnels ne nécessitent pas non plus d'immuabilité. Clojure a une annotation spécifique qui permet aux types d'être mutables, et la plupart des langages fonctionnels "pratiques" ont un moyen de spécifier des types mutables.
Une meilleure question à poser pourrait être "L'immuabilité complète est-elle logique dans la programmation impérative?" Je dirais la réponse évidente à cette question est non. Pour obtenir une immuabilité totale dans la programmation impérative, vous devez renoncer à des choses comme for
boucles (car vous devez muter une variable de boucle) en faveur de la récursivité, et maintenant vous programmez essentiellement de manière fonctionnelle de toute façon .
Il est souvent utile de classer les objets en encapsulant des valeurs ou des entités, la distinction étant que si quelque chose est une valeur, le code qui y fait référence ne devrait jamais voir son état changer d'une manière que le code lui-même n'a pas initiée. En revanche, le code qui contient une référence à une entité peut s'attendre à ce qu'elle change de façon indépendante de la volonté du titulaire de la référence.
Bien qu'il soit possible d'utiliser une valeur d'encapsulation à l'aide d'objets de types mutables ou immuables, un objet ne peut se comporter comme une valeur que si au moins une des conditions suivantes s'applique:
Aucune référence à l'objet ne sera jamais exposée à quoi que ce soit qui pourrait changer l'état encapsulé dans celui-ci.
Le détenteur d'au moins une des références à l'objet connaît toutes les utilisations possibles de toute référence existante.
Étant donné que toutes les instances de types immuables satisfont automatiquement la première exigence, leur utilisation en tant que valeurs est facile. Il est en revanche beaucoup plus difficile de garantir que l'une ou l'autre de ces exigences est satisfaite lors de l'utilisation de types mutables. Alors que les références aux types immuables peuvent être librement transmises comme moyen d'encapsuler l'état encapsulé dans celles-ci, le passage de l'état stocké dans des types mutables nécessite soit la construction d'objets enveloppants immuables, soit la copie de l'état encapsulé par des objets privés dans d'autres objets qui sont soit fourni par ou construit pour le destinataire des données.
Les types immuables fonctionnent très bien pour transmettre des valeurs et sont souvent au moins quelque peu utilisables pour les manipuler. Cependant, ils ne sont pas si bons pour gérer les entités. La chose la plus proche que l'on puisse avoir d'une entité dans un système avec des types purement immuables est une fonction qui, compte tenu de l'état du système, signalera les attributs d'une partie de celui-ci, ou produira une nouvelle instance d'état du système qui ressemble à un fourni un sauf pour une partie particulière de celui-ci qui sera différent d'une certaine manière sélectionnable. De plus, si le but d'une entité est d'interfacer du code à quelque chose qui existe dans le monde réel, il peut être impossible pour l'entité d'éviter d'exposer un état mutable.
Par exemple, si l'on reçoit des données sur une connexion TCP, on pourrait produire un nouvel objet "état du monde" qui inclut ces données dans son tampon sans affecter aucune référence à l'ancien "état") du monde ", mais les anciennes copies de l'état du monde qui n'incluent pas le dernier lot de données seront défectueuses et ne devraient pas être utilisées car elles ne correspondront plus à l'état du monde réel TCP socket.
En c # certains types sont immuables comme string.
Cela semble en outre suggérer que le choix a été fortement envisagé.
Bien sûr, c'est vraiment exigeant en termes de performances d'utiliser des types immuables si vous devez modifier ce type des centaines de milliers de fois. C'est la raison pour laquelle il est suggéré d'utiliser la classe StringBuilder
au lieu de la classe string
dans ce cas.
J'ai fait une expérience avec un profileur et utiliser le type immuable est vraiment plus CPU et RAM exigeant.
C'est également intuitif si vous considérez que pour modifier une seule lettre dans une chaîne de 4000 caractères, vous devez copier chaque caractère dans une autre zone de la RAM.
L'immuabilité complète de tout n'a pas beaucoup de sens dans la POO, ou la plupart des autres paradigmes d'ailleurs, pour une très grande raison:
Chaque programme utile a des effets secondaires.
Un programme qui ne fait rien changer ne vaut rien. Vous pouvez tout aussi bien ne pas l'avoir exécuté, car l'effet sera identique.
Même si vous pensez que vous ne changez rien et que vous résumez simplement une liste de chiffres que vous avez reçus, considérez que vous devez faire quelque chose avec le résultat - que vous l'imprimiez sur une sortie standard, l'écriviez dans un fichier, ou n'importe où. Et cela implique de muter un tampon et de changer l'état du système.
Il peut être très judicieux de restreindre la mutabilité aux parties qui doivent pouvoir changer. Mais si absolument rien ne doit changer, alors vous ne faites rien qui vaille la peine.