Ne déclarez pas les interfaces pour les objets immuables

27

Ne déclarez pas les interfaces pour les objets immuables

[EDIT] Lorsque les objets en question représentent des objets de transfert de données (DTO) ou des données anciennes (POD)

Est-ce une directive raisonnable?

Jusqu'à présent, j'ai souvent créé des interfaces pour des classes scellées qui sont immuables (les données ne peuvent pas être modifiées). J'ai moi-même essayé de ne pas utiliser l'interface où je me soucie de l'immuabilité.

Malheureusement, l'interface commence à pénétrer le code (et ce n'est pas seulement mon code qui m'inquiète). Vous finissez par passer une interface, puis vous voulez la passer à un code qui veut vraiment supposer que la chose qui lui est passée est immuable.

En raison de ce problème, j'envisage de ne jamais déclarer d'interfaces pour des objets immuables.

Cela pourrait avoir des ramifications en ce qui concerne les tests unitaires, mais à part cela, cela semble-t-il une directive raisonnable?

Ou existe-t-il un autre modèle que je devrais utiliser pour éviter le problème "d'interface d'étalement" que je vois?

(J'utilise ces objets immuables pour plusieurs raisons: principalement pour la sécurité des threads car j'écris beaucoup de code multithread; mais aussi parce que cela signifie que je peux éviter de faire des copies défensives des objets passés aux méthodes. Le code devient beaucoup plus simple dans de nombreux cas où vous savez que quelque chose est immuable - ce que vous ne faites pas si vous avez reçu une interface. En fait, souvent vous ne pouvez même pas faire une copie défensive d'un objet référencé via une interface s'il ne fournit pas de opération de clonage ou tout moyen de le sérialiser ...)

[MODIFIER]

Pour fournir beaucoup plus de contexte à mes raisons de vouloir rendre les objets immuables, voir ce billet de blog d'Eric Lippert:

http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/

Je dois également souligner que je travaille avec certains concepts de niveau inférieur ici, tels que les éléments qui sont manipulés / transmis dans des files d'attente de travaux multithreads. Ce sont essentiellement des DTO.

Joshua Bloch recommande également l'utilisation d'objets immuables dans son livre Effective Java .


Suivre

Merci pour la rétroaction, tout. J'ai décidé d'aller de l'avant et d'utiliser cette directive pour les DTO et leurs semblables. Ça marche bien jusqu'à présent, mais ça ne fait qu'une semaine ... Pourtant, ça a l'air bien.

Il y a d'autres questions à ce sujet que je veux poser; notamment quelque chose que j'appelle "Immuabilité profonde ou superficielle" (nomenclature que j'ai volée du clonage Deep and Shallow) - mais c'est une question pour une autre fois.

Matthew Watson
la source
3
+1 grande question. Veuillez expliquer pourquoi il est si important pour vous que l'objet soit de cette classe immuable particulière plutôt que quelque chose qui implémente simplement la même interface. Il est possible que vos problèmes soient enracinés ailleurs. L'injection de dépendance est extrêmement importante pour les tests unitaires, mais elle présente de nombreux autres avantages importants qui disparaissent tous si vous forcez votre code à exiger une classe immuable particulière.
Steven Doggart
1
Je suis légèrement troublé par votre utilisation du terme immuable . Juste pour être clair, voulez-vous dire une classe scellée, qui ne peut pas être héritée et remplacée, ou voulez-vous dire que les données qu'elle expose sont en lecture seule et ne peuvent pas être modifiées une fois l'objet créé. En d'autres termes, voulez-vous garantir que l'objet est d'un type particulier, ou que sa valeur ne changera jamais. Dans mon premier commentaire, j'ai supposé signifier le premier, mais maintenant avec votre montage, cela ressemble plus à ce dernier. Ou êtes-vous concerné par les deux?
Steven Doggart
3
Je suis confus, pourquoi ne pas simplement laisser les setters hors de l'interface? Mais d'un autre côté, je me lasse de voir des interfaces pour des objets qui sont vraiment des objets de domaine et / ou des DTO nécessitant des usines pour ces objets ... provoque une dissonance cognitive pour moi.
Michael Brown
2
Qu'en est-il des interfaces pour les classes qui sont toutes immuables? Par exemple, Java Nombre classe permet de définir un un List<Number>qui peut contenir Integer, Float, Long, BigDecimal, etc ... qui sont eux - mêmes immuables.
1
@MikeBrown Je suppose que ce n'est pas parce que l'interface implique l'immuabilité que l'implémentation l'impose. Vous pouvez simplement le laisser à la convention (c'est-à-dire documenter l'interface que l'immuabilité est une exigence) mais vous pourriez vous retrouver avec des problèmes vraiment désagréables si quelqu'un a violé la convention.
vaughandroid

Réponses:

17

À mon avis, votre règle est bonne (ou du moins ce n'est pas une mauvaise), mais uniquement à cause de la situation que vous décrivez. Je ne dirais pas que je suis d'accord avec lui dans toutes les situations, donc, du point de vue de mon pédant intérieur, je dois dire que votre règle est techniquement trop large.

Généralement, vous ne définiriez pas d'objets immuables à moins qu'ils ne soient essentiellement utilisés comme objets de transfert de données (DTO), ce qui signifie qu'ils contiennent des propriétés de données mais très peu de logique et aucune dépendance. Si tel est le cas, comme il semble qu'il soit ici, je dirais que vous pouvez utiliser les types concrets en toute sécurité plutôt que les interfaces.

Je suis sûr qu'il y aura des puristes de tests unitaires qui seront en désaccord, mais à mon avis, les classes DTO peuvent être exclues en toute sécurité des tests unitaires et des exigences d'injection de dépendance. Il n'est pas nécessaire d'utiliser une usine pour créer un DTO, car il n'a pas de dépendances. Si tout crée les DTO directement selon les besoins, alors il n'y a vraiment aucun moyen d'injecter un type différent de toute façon, donc il n'y a pas besoin d'interface. Et comme ils ne contiennent aucune logique, il n'y a rien à tester. Même s'ils contiennent de la logique, tant qu'ils n'ont pas de dépendances, il devrait être trivial de tester la logique unitaire, si nécessaire.

En tant que tel, je pense que faire une règle selon laquelle toutes les classes DTO ne doivent pas implémenter une interface, bien que potentiellement inutile, ne nuira pas à la conception de votre logiciel. Puisque vous avez cette exigence selon laquelle les données doivent être immuables et que vous ne pouvez pas les appliquer via une interface, je dirais qu'il est tout à fait légitime d'établir cette règle comme norme de codage.

Le plus gros problème, cependant, est la nécessité d'appliquer strictement une couche DTO propre. Tant que vos classes immuables sans interface n'existent que dans la couche DTO et que votre couche DTO reste exempte de logique et de dépendances, vous serez en sécurité. Si vous commencez à mélanger vos couches et que vous avez des classes sans interface qui se doublent de classes de couches métier, je pense que vous commencerez à avoir beaucoup de problèmes.

Steven Doggart
la source
Le code qui s'attend à traiter spécifiquement un DTO ou un objet de valeur immuable devrait utiliser les fonctionnalités de ce type plutôt que celles d'une interface, mais cela ne signifie pas qu'il n'y aurait pas de cas d'utilisation pour une interface implémentée par une telle classe et aussi par d'autres classes, avec des méthodes qui promettent de renvoyer des valeurs qui seront valides pendant un temps minimum (par exemple, si l'interface est passée à une fonction, les méthodes doivent renvoyer des valeurs valides jusqu'à ce que cette fonction revienne). Une telle interface peut être utile s'il existe un certain nombre de types qui encapsulent des données similaires et que l'on souhaite ...
supercat
... pour avoir un moyen de copier les données de l'une à l'autre. Si tous les types qui incluent certaines données implémentent la même interface pour les lire, alors ces données peuvent être copiées facilement parmi tous les types, sans que chacun sache comment importer à partir de chacun des autres.
supercat
À ce stade, vous pouvez également éliminer vos getters (et setters, si vos DTO sont mutables pour une raison stupide) et rendre les champs publics. Vous n'allez jamais mettre de logique là-dedans, non?
Kevin
@ Kevin, je suppose, mais avec la commodité moderne des propriétés automobiles, il est si facile d'en faire des propriétés, pourquoi pas? Je suppose que si les performances et l'efficacité sont de la plus haute importance, alors peut-être que cela compte. En tout cas, la question est, quel niveau de "logique" autorisez-vous dans un DTO? Même si vous mettez une logique de validation dans ses propriétés, ce ne serait pas un problème de le tester à l'unité tant qu'il n'aurait pas de dépendances nécessitant une simulation. Tant que le DTO n'utilise aucun objet métier de dépendance, il est sûr d'avoir un peu de logique intégré, car ils seront toujours testables.
Steven Doggart
Personnellement, je préfère les garder aussi propres que possible de ce genre de choses, mais il y a toujours des moments où il est nécessaire de contourner les règles et il est donc préférable de laisser la porte ouverte au cas où.
Steven Doggart du
7

J'avais l'habitude de faire beaucoup d'histoires pour rendre mon code invulnérable à une mauvaise utilisation. J'ai créé des interfaces en lecture seule pour cacher les membres en mutation, ajouté beaucoup de contraintes à mes signatures génériques, etc., etc. Il s'est avéré que la plupart du temps, je prenais des décisions de conception parce que je ne faisais pas confiance à mes collègues imaginaires. "Peut-être qu'un jour, ils embaucheront un nouveau type d'entrée de gamme et il ne saura pas que la classe XYZ ne peut pas mettre à jour DTO ABC. Oh non!" D'autres fois, je me concentrais sur le mauvais problème - ignorer la solution évidente - ne pas voir la forêt à travers les arbres.

Je ne crée plus d'interfaces pour mes DTO. Je travaille en supposant que les personnes qui touchent mon code (principalement moi-même) savent ce qui est autorisé et ce qui a du sens. Si je continue à faire la même erreur stupide, je n'essaie généralement pas de durcir mes interfaces. Maintenant, je passe la plupart de mon temps à essayer de comprendre pourquoi je continue de faire la même erreur. C'est généralement parce que je sur-analyse quelque chose ou que je manque un concept clé. Mon code a été beaucoup plus facile à utiliser depuis que j'ai renoncé à être paranoïaque. Je me retrouve également avec moins de «cadres» qui nécessitaient les connaissances d'un initié pour travailler sur le système.

Ma conclusion a été de trouver la chose la plus simple qui fonctionne. La complexité supplémentaire de la création d'interfaces sûres ne fait que gaspiller du temps de développement et complique le code autrement simple. Vous vous inquiétez de ce genre de choses lorsque 10 000 développeurs utilisent vos bibliothèques. Croyez-moi, cela vous évitera bien des tensions inutiles.

Parcs Travis
la source
J'enregistre généralement des interfaces lorsque j'ai besoin d'une injection de dépendance pour les tests unitaires.
Travis Parks
5

Cela semble être une directive correcte, mais pour des raisons étranges. J'ai eu un certain nombre d'endroits où une interface (ou une classe de base abstraite) fournit un accès uniforme à une série d'objets immuables. Les stratégies ont tendance à tomber ici. Les objets d'état ont tendance à tomber ici. Je ne pense pas qu'il soit trop déraisonnable de façonner une interface pour qu'elle semble immuable et de la documenter comme telle dans votre API.

Cela dit, les gens ont tendance à sur-interfacer les objets Plain Old Data (ci-après, POD), et même les structures simples (souvent immuables). Si votre code n'a pas d'alternative saine à une structure fondamentale, il n'a pas besoin d'une interface. Non, les tests unitaires ne sont pas une raison suffisante pour changer votre conception (se moquer de l'accès à la base de données n'est pas la raison pour laquelle vous fournissez une interface à cela, c'est la flexibilité pour les changements futurs) - ce n'est pas la fin du monde si vos tests utiliser cette structure fondamentale de base telle quelle.

Telastyn
la source
2

Le code devient beaucoup plus simple dans de nombreux cas lorsque vous savez que quelque chose est immuable - ce que vous ne faites pas si vous avez reçu une interface.

Je ne pense pas que ce soit une préoccupation dont l'implémenteur accesseur doit s'inquiéter. Si interface Xest censé être immuable, n'est-il pas de la responsabilité de l'implémenteur d'interface de s'assurer qu'il implémente l'interface de manière immuable?

Cependant, dans mon esprit, il n'y a rien de tel qu'une interface immuable - le contrat de code spécifié par une interface s'applique uniquement aux méthodes exposées d'un objet, et rien sur les internes d'un objet.

Il est beaucoup plus courant de voir l'immuabilité implémentée comme un décorateur plutôt qu'une interface, mais la faisabilité de cette solution dépend vraiment de la structure de votre objet et de la complexité de votre implémentation.

Jonathan Rich
la source
1
Pourriez-vous fournir un exemple d'implémentation de l'immuabilité en tant que décorateur?
vaughandroid
Pas trivialement, je ne pense pas, mais c'est un exercice de réflexion relativement simple - si MutableObjecta des nméthodes qui changent d'état et des mméthodes qui retournent l'état, ImmutableDecoratorpeut continuer à exposer les méthodes qui retournent state ( m), et, selon l'environnement, affirmer ou lève une exception lorsque l'une des méthodes mutables est appelée.
Jonathan Rich
Je n'aime vraiment pas convertir la certitude au moment de la compilation en possibilité d'exceptions au moment de l'exécution ...
Matthew Watson
OK, mais comment ImmutableDecorator peut-il savoir si une méthode donnée change d'état ou non?
vaughandroid
Vous ne pouvez en aucun cas être certain qu'une classe qui implémente votre interface est immuable en ce qui concerne les méthodes définies par votre interface. @Baqueta Le décorateur doit avoir une connaissance de l'implémentation de la classe de base.
Jonathan Rich
0

Vous finissez par passer une interface, puis vous voulez la passer à un code qui veut vraiment supposer que la chose qui lui est passée est immuable.

En C #, une méthode qui attend un objet de type "immuable" ne doit pas avoir de paramètre de type d'interface car les interfaces C # ne peuvent pas spécifier le contrat d'immuabilité. Par conséquent, la directive que vous proposez elle-même n'a pas de sens en C # car vous ne pouvez pas le faire en premier lieu (et les futures versions des langages ne vous permettront probablement pas de le faire).

Votre question découle d'une incompréhension subtile des articles d'Eric Lippert sur l'immuabilité. Eric n'a pas défini les interfaces IStack<T>et IQueue<T>spécifié les contrats pour les piles et files d'attente immuables. Ils ne le font pas. Il les a définis par commodité. Ces interfaces lui ont permis de définir différents types de piles et de files d'attente vides. Nous pouvons proposer une conception et une implémentation différentes d'une pile immuable en utilisant un seul type sans nécessiter d'interface ou un type séparé pour représenter la pile vide, mais le code résultant ne sera pas aussi propre et serait un peu moins efficace.

Restons-en à la conception d'Eric. Une méthode qui nécessite une pile immuable doit avoir un paramètre de type Stack<T>plutôt que l'interface générale IStack<T>qui représente le type de données abstrait d'une pile au sens général. Il n'est pas évident de savoir comment faire cela lors de l'utilisation de la pile immuable d'Eric et il n'en a pas parlé dans ses articles, mais c'est possible. Le problème vient du type de la pile vide. Vous pouvez résoudre ce problème en vous assurant que vous n'obtiendrez jamais une pile vide. Cela peut être assuré en poussant une valeur fictive comme première valeur sur la pile et en ne la sautant jamais. De cette façon, vous pouvez diffuser en toute sécurité les résultats de Pushet Popvers Stack<T>.

Avoir des Stack<T>outils IStack<T>peut être utile. Vous pouvez définir des méthodes qui nécessitent une pile, n'importe quelle pile, pas nécessairement une pile immuable. Ces méthodes peuvent avoir un paramètre de type IStack<T>. Cela vous permet de lui transmettre des piles immuables. Idéalement, IStack<T>ferait partie de la bibliothèque standard elle-même. Dans .NET, il n'y en a pas IStack<T>, mais il existe d'autres interfaces standard que la pile immuable peut implémenter, ce qui rend le type plus utile.

La conception et la mise en œuvre alternatives de la pile immuable dont j'ai parlé précédemment utilisent une interface appelée IImmutableStack<T>. Bien sûr, l'insertion de "Immutable" dans le nom de l'interface ne rend pas immuable tous les types qui l'implémentent. Cependant, dans cette interface, le contrat d'immuabilité n'est que verbal. Un bon développeur doit le respecter.

Si vous développez une petite bibliothèque interne, vous pouvez vous mettre d'accord avec tout le monde sur l'équipe pour honorer ce contrat et vous pouvez utiliser IImmutableStack<T>comme type de paramètres. Sinon, vous ne devez pas utiliser le type d'interface.

Je voudrais ajouter, puisque vous avez marqué la question C #, que dans la spécification C #, il n'y a rien de tel que les DTO et les POD. Par conséquent, les jeter ou les définir précisément améliore la question.

Hadi Brais
la source