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.
la source
List<Number>
qui peut contenirInteger
,Float
,Long
,BigDecimal
, etc ... qui sont eux - mêmes immuables.Réponses:
À 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.
la source
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.
la source
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.
la source
Je ne pense pas que ce soit une préoccupation dont l'implémenteur accesseur doit s'inquiéter. Si
interface X
est 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.
la source
MutableObject
a desn
méthodes qui changent d'état et desm
méthodes qui retournent l'état,ImmutableDecorator
peut 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.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>
etIQueue<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éraleIStack<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 dePush
etPop
versStack<T>
.Avoir des
Stack<T>
outilsIStack<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 typeIStack<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 pasIStack<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.
la source