Objets contre structures de données

Dans l'article, dont la traduction est proposée ci-dessous, Robert Martin semble commencer par des pensées très similaires à celles que l'on peut voir dans les discussions de Yegor Bugaenko sur l'ORM, mais d'autres tirent des conclusions. Personnellement, l'approche de Yegor m'impressionne, mais je pense que Martin révèle le sujet plus en détail. Il me semble que cela vaut la peine de se familiariser avec tous ceux qui ont déjà réfléchi à la place qu'ORM devrait prendre et, en général, pourquoi nous avons besoin d'objets dans lesquels tous les champs sont ouverts. L'article est écrit dans le genre "Dialogue", où un programmeur plus expérimenté discute d'un problème avec quelqu'un qui a moins d'expérience.


Qu'est-ce qu'une classe?

Une classe est une spécification de nombreux objets similaires.


Qu'est-ce qu'un objet?

Un objet est un ensemble de fonctions qui effectuent des actions avec des données encapsulées.


Ou vaut-il mieux dire qu'un objet est un ensemble de fonctions qui effectuent des actions avec des données dont l'existence est implicite

Dans le sens de "implicite"?


Une fois que l'objet a des fonctions, on peut supposer qu'il y a aussi des données, mais il n'y a pas d'accès direct aux données et elles ne sont pas du tout visibles de l'extérieur.

Les données ne sont-elles pas dans l'objet?


Peut-être qu'ils le sont, mais la règle qui dit qu'ils doivent être là ne l'est pas. Du point de vue de l'utilisateur, un objet n'est rien d'autre qu'un ensemble de fonctions. Les données avec lesquelles ces fonctions fonctionnent doivent exister, mais la position de ces données est inconnue de l'utilisateur.

Eh bien, disons.


Eh bien, qu'est-ce qu'une structure de données?

Une structure de données est une collection d'éléments associés.


Ou, en d'autres termes, une structure de données est un ensemble d'éléments avec lesquels les fonctions fonctionnent, dont l'existence est implicite implicitement.

D'accord, d'accord. Je comprends. Les fonctions qui fonctionnent avec les structures de données ne sont pas définies à l'intérieur de ces structures, mais de l'existence même d'une structure de données, nous pouvons conclure qu'il doit y avoir quelque chose qui fonctionne avec elles.


Oui. Et qu'en est-il de ces deux définitions?

Dans un sens, ils sont opposés les uns aux autres.


Vraiment. Ils se complètent. Comme une main et un gant.
  • Un objet est un ensemble de fonctions qui fonctionnent avec des éléments de données dont l'existence est implicite implicitement.
  • Une structure de données est un ensemble d'éléments de données avec lesquels les fonctions fonctionnent, dont l'existence est implicite implicitement.

Ouah! Il s'avère donc que les objets et les structures de données ne sont pas la même chose!


Oui. Les structures de données sont des DTO.

Et les tables des bases de données ne sont pas non plus des objets, non?


Encore vrai. Les bases de données contiennent des structures de données, pas des objets.

Attends un instant. ORM ne mappe-t-il pas les tables de la base de données aux objets?


Bien sûr que non. Vous ne pouvez pas mapper des tables de base de données à des objets. Les tables de la base de données sont des structures de données, pas des objets.

Alors que fait ORM?


Ils transfèrent des données d'une structure à une autre.

Ils n'ont donc rien à voir avec les objets?


Rien du tout. À proprement parler, une chose comme ORM au sens de la technologie qui mappe les données relationnelles aux objets n'existe pas, car les tables ne peuvent pas être mappées d'une base de données à des objets.

Mais ils m'ont dit que les ORM collectent des objets métier.


Non, ORM récupère les données d'une base de données avec laquelle les objets métier fonctionnent

Mais ces structures de données ne tombent-elles pas dans des objets métier?


Peut-être qu'ils obtiennent, ou peut-être pas. ORM n'en sait rien.

Mais la différence est purement sémantique.


Non, non. Il y a des conséquences d'une grande portée.

Par exemple?


Par exemple, la conception du schéma de base de données et la conception d'objets métier. Les objets métier définissent le comportement métier. Le schéma de base de données définit la structure des données métier. Ces structures sont limitées par des forces très différentes. Une structure de données d'entreprise n'est pas nécessairement la meilleure structure pour le comportement d'une entreprise.

Eeee. C'est incompréhensible.


Pensez-y de cette façon. Le schéma de données n'est pas conçu pour une seule application, il est destiné à être utilisé dans toute l'entreprise. Par conséquent, la structure des données est un compromis entre plusieurs applications différentes.

C'est compréhensible.


Bon. Pensez maintenant à chaque application. Le modèle objet de chaque application décrit comment le comportement de l'application est structuré. Chaque application aura son propre modèle d'objet pour mieux correspondre au comportement de l'application.

Ahh, je vois. Étant donné que le schéma de données est un compromis entre différentes applications, le schéma ne tombera pas sur le modèle objet de chaque application individuelle.


Ok! Les objets et les structures sont limités à différentes choses. Ils s'assemblent très rarement. Les gens appellent cela l'inadéquation de l'impédance relationnelle-objet.

Quelque chose dont je me souviens. Mais il semble que l'inadéquation de l'impédance vient d'être corrigée à l'aide de l'ORM.


Et maintenant, vous savez que ce n'est pas le cas. L'inadéquation d'impédance entre les objets et les structures de données est complémentaire et non isomorphe.

Quoi?


Ce sont des opposés, pas quelque chose de similaire.

Opposés?


Oui, dans un sens très intéressant. Vous voyez, les objets et les structures de données impliquent des structures de contrôle diamétralement opposées.

Quoi?


Pensez à un ensemble de classes qui implémentent une sorte d'interface commune. Par exemple, imaginez des classes qui représentent des figures à deux dimensions, dans lesquelles il existe des fonctions pour calculer l'aire et le périmètre d'une figure.

Dans quelle mesure les formes poussent-elles du code avec des objets dans tous les exemples?


Examinons deux types de formes différents: les carrés et les cercles. Il est clair que les fonctions de calcul de l'aire et du périmètre de ces classes utilisent des structures de données différentes. Il est également entendu que ces opérations sont appelées par polymorphisme dynamique.

Ralentissez s'il vous plaît, rien n'est clair.


Il existe deux fonctions différentes pour calculer l'aire, une pour le carré, l'autre pour le cercle. Lorsqu'une fonction est appelée pour calculer l'aire d'un objet particulier, c'est cet objet qui décide quelle fonction particulière appeler. C'est ce qu'on appelle le polymorphisme dynamique.

D'accord. Bien sûr. Un objet sait comment ses méthodes sont implémentées. Naturellement.


Transformons maintenant ces objets en structures de données. Nous utilisons des syndicats discriminés.

Discriminé quoi?


Syndicats discriminés. Eh bien, C ++, des pointeurs, le mot-clé union, un indicateur pour déterminer le type de structure, Discriminated Unions. Dans notre cas, ce ne sont que deux structures de données différentes. Un pour le carré et un pour le cercle. Le cercle a un point central et un rayon. Et un code de type à partir duquel on peut comprendre qu'il s'agit d'un cercle.

Le champ avec le code sera enum?


Et bien oui. Et le carré aura le point supérieur gauche et la longueur du côté. Et aussi énumérer pour indiquer le type.

D'accord. Il y aura deux structures avec un code de type.


Oui. Examinons maintenant la fonction de la zone. Il y aura probablement un interrupteur, non?

Et bien. Bien sûr, pour deux classes. La Branche du Carré et du Cercle. Et pour le périmètre, vous avez également besoin d'un interrupteur similaire.


Et encore une fois, à droite. Réfléchissez maintenant à ces deux scénarios. Dans un scénario avec des objets, deux implémentations de fonctions pour une zone sont indépendantes l'une de l'autre et appartiennent (dans un sens) directement au type. La fonction de l'aire du carré appartient au carré et la fonction de détermination de l'aire du cercle appartient au cercle.

D'accord, je comprends ce que vous menez. Dans un scénario avec des structures de données, les deux implémentations d'une fonction pour une zone sont dans la même fonction, elles «n'appartiennent» pas (quel que soit le sens de ce mot) au type.


Plus c'est mieux. Dans le cas des objets, si vous devez ajouter le type Triangle, quel code doit être changé?

Ne changez rien du tout. Créez simplement une nouvelle classe Triangle. Bien que non, vous devez probablement corriger le code qui crée les objets.


Oui. Ainsi, lors de l'ajout d'un nouveau type, les modifications sont négligeables. Supposons maintenant que vous ayez besoin d'ajouter une nouvelle fonction - par exemple, une fonction pour déterminer le centre.

Ensuite, vous devez l'ajouter aux trois types, Circle, Square et Triangle.


Bon. Il s'avère que l'ajout de nouvelles fonctions est difficile, car vous devez apporter des modifications dans chaque classe.

Mais avec les structures de données, tout est différent. Pour ajouter un triangle, vous devez modifier chaque fonction pour ajouter des branches pour gérer le triangle dans chaque commutateur.


Oui. Il est difficile d'ajouter des types; vous devez modifier chaque fonction.

Mais pour ajouter une fonction au centre, rien ne doit être changé.


Oui, l'ajout de fonctionnalités est facile.

Ouah. Il s'avère que ces deux approches sont directement opposées.


Certainement oui. Pour résumer

  • Il est difficile d'ajouter de nouvelles fonctions aux classes, vous devez faire des changements dans chaque classe
  • L'ajout de nouvelles fonctions aux structures de données est simple, il vous suffit d'ajouter une fonction, rien d'autre ne doit être changé
  • L'ajout de nouveaux types aux classes est simple, il vous suffit d'ajouter une nouvelle classe
  • Il est difficile d'ajouter de nouveaux types pour les structures; vous devez corriger chaque fonction

Oui Opposés. Opposés dans un sens curieux. Autrement dit, si l'on sait à l'avance que de nouvelles fonctions doivent être ajoutées, il est commode d'utiliser des structures de données. Mais si vous savez à l'avance que vous devez ajouter de nouveaux types, vous devez utiliser des classes.


Bonne observation! Mais aujourd'hui, nous devons penser à autre chose. Il y a un autre point où les structures de données et les classes sont opposées les unes aux autres. Dépendances.

Addictions?


Oui, la direction des dépendances dans le code source.

D'accord, je vais demander. Quelle est la différence?


Regardons le cas des structures. Chaque fonction contient un commutateur qui sélectionne l'implémentation souhaitée en fonction du code de type dans l'union.

Oui, ça l'est. Et alors?


Examinons l'appel de fonction pour la zone. Le code appelant dépend de la fonction de la zone et la fonction de la zone dépend de chaque implémentation spécifique.

Et que voulez-vous dire quand vous dites "dépend"?


Imaginez que chaque implémentation d'une fonction pour une zone soit affectée à une fonction distincte. Autrement dit, il y aura des fonctions circleArea, squareArea et triangleArea.

Eh bien, il s'avère que dans les branches de commutation, il y aura simplement des appels à ces fonctions.


Imaginez que ces fonctions se trouvent dans des fichiers différents.

Ensuite, dans le fichier avec le commutateur sera importé ou utilisé ou inclus pour les fichiers avec des fonctions.


À droite Il s'agit d'une dépendance au niveau du code source. Une source dépend d'une autre source. Comment est dirigée cette dépendance?

Le code source avec commutateur dépend du code source dans lequel se trouvent les implémentations.


Qu'en est-il du code appelant la fonction pour la zone?

Le code appelant dépend du code avec switch, qui dépend de toutes les implémentations.


Oui. Dans toutes les sources, la flèche est dirigée dans le sens de l'appel, du code appelant à l'implémentation. Donc, si vous voulez faire un petit changement dans ces implémentations ...

D'accord, d'accord, je vois où tu veux en venir. Un changement dans l'une des implémentations entraînera la recompilation de tous les fichiers avec switch, et cela conduira au fait que tout ce qui appelle ce switch sera recompilé, par exemple, dans notre cas, la fonction pour la zone.


Oui Ce sera du moins le cas pour les langues qui utilisent des dates de modification de fichier afin de comprendre ce qui doit être reconstruit.

Et ce sont généralement tous les systèmes avec typage statique, non?


Oui, et certains autres systèmes sans lui

Cela doit être reconstruit beaucoup.


Et beaucoup à refaire.

D'accord, mais dans le cas des cours, est-ce l'inverse?


Oui, car le code qui appelle la fonction de la zone dépend de l'interface et l'implémentation dépend également de cette interface.

Je vois. Le code de la classe Square importera ou utilisera ou inclura un fichier avec l'interface Shape.


Oui. La flèche dans les fichiers d'implémentation pointe dans la direction opposée à l'appel. Il est dirigé du code d'implémentation vers le code d'appel. Ce sera du moins le cas pour les langues typées statiquement. Pour les langages typés dynamiquement, le code qui appelle la fonction pour la zone ne dépend de rien du tout, car la liaison se produit lors de l'exécution.

Ouais, d'accord. Autrement dit, si vous apportez des modifications à l'une des implémentations ...


Il est seulement nécessaire de reconstruire et de réinstaller le code avec ces modifications.

En effet, les dépendances sont dirigées à l'opposé de la direction des appels.


Oui, nous l'appelons inversion de dépendance.

D'accord, résumons tout. Les classes et les structures de données sont opposées les unes aux autres dans trois sens.


  • Les fonctions sont explicitement dans les classes et vous ne pouvez que deviner l'existence de données. Les structures de données sont explicitement présentes dans les structures de données, et vous ne pouvez que deviner quelles fonctions sont disponibles.
  • Dans le cas des classes, l'ajout de types est simple, mais l'ajout de fonctions est difficile. Dans le cas des structures, l'ajout de fonctions est facile, mais l'ajout de types est difficile.
  • Les structures de données entraînent une recompilation et une redistribution du code appelant. Les classes isolent le code appelant et n'ont pas besoin de recompiler et de le déployer à nouveau.

Oui, c'est vrai. Et cela devrait être gardé à l'esprit par chaque concepteur et architecte logiciel.

Source: https://habr.com/ru/post/fr477712/


All Articles