J'ai de gros objets (plus de 3 champs) qui peuvent et doivent être immuables. Chaque fois que je rencontre ce cas, j'ai tendance à créer des abominations de constructeur avec de longues listes de paramètres.
Cela ne semble pas correct, il est difficile à utiliser et la lisibilité en souffre.
C'est encore pire si les champs sont une sorte de type de collection comme des listes. Un simple addSibling(S s)
faciliterait tellement la création d'objet mais rendrait l'objet mutable.
Qu'est-ce que vous utilisez dans de tels cas?
Je suis sur Scala et Java, mais je pense que le problème est indépendant du langage tant que le langage est orienté objet.
Des solutions auxquelles je peux penser:
- "Abominations de constructeur avec de longues listes de paramètres"
- Le modèle du constructeur
java
oop
scala
immutability
Malax
la source
la source
Réponses:
Eh bien, vous voulez à la fois un objet plus facile à lire et immuable une fois créé?
Je pense qu'une interface fluide CORRECTEMENT FAITE vous aiderait.
Cela ressemblerait à ceci (exemple purement inventé):
J'ai écrit «CORRECTEMENT FAIT» en gras parce que la plupart des programmeurs Java se trompent d'interface couramment et polluent leur objet avec la méthode nécessaire pour construire l'objet, ce qui est bien sûr complètement faux.
L'astuce est que seule la méthode build () crée réellement un Foo (donc vous Foo peut être immuable).
FooFactory.create () , whereXXX (..) et withXXX (..) créent tous «autre chose».
Ce quelque chose d'autre peut être une FooFactory, voici une façon de le faire ...
Votre FooFactory ressemblerait à ceci:
la source
Foo
objet, plutôt que d'en avoir une autreFooFactory
.FooImpl
constructeur avec 8 paramètres. Quelle est l'amélioration?immutable
et j'aurais peur que les gens réutilisent des objets d'usine parce qu'ils pensent que c'est le cas. Je veux dire:FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();
oùtalls
ne contiendrait plus que des femmes. Cela ne devrait pas se produire avec une API immuable.Dans Scala 2.8, vous pouvez utiliser des paramètres nommés et par défaut ainsi que la
copy
méthode sur une classe de cas. Voici un exemple de code:la source
Eh bien, considérez ceci sur Scala 2.8:
Cela a son lot de problèmes, bien sûr. Par exemple, essayez de faire
espouse
etOption[Person]
, puis de marier deux personnes. Je ne peux pas penser à un moyen de résoudre cela sans recourir à unprivate var
et / ou unprivate
constructeur plus une usine.la source
Voici quelques autres options:
Option 1
Rendez l'implémentation elle-même mutable, mais séparez les interfaces auxquelles elle expose mutable et immuable. Ceci est tiré de la conception de la bibliothèque Swing.
Option 2
Si votre application contient un ensemble important mais prédéfini d'objets immuables (par exemple, des objets de configuration), vous pouvez envisager d'utiliser le framework Spring .
la source
Il est utile de se rappeler qu'il existe différents types d'immuabilité . Pour votre cas, je pense que l'immuabilité "popsicle" fonctionnera très bien:
Donc, vous initialisez votre objet, puis définissez un indicateur de "gel" d'une sorte indiquant qu'il n'est plus accessible en écriture. De préférence, vous cacheriez la mutation derrière une fonction afin que la fonction soit toujours pure pour les clients consommant votre API.
la source
clone()
pour dériver de nouvelles instances.freeze()
méthode, les choses pourraient devenir laides.Vous pouvez également faire en sorte que les objets immuables exposent des méthodes qui ressemblent à des mutateurs (comme addSibling) mais les laisser renvoyer une nouvelle instance. C'est ce que font les collections immuables Scala.
L'inconvénient est que vous pouvez créer plus d'instances que nécessaire. Il n'est également applicable que lorsqu'il existe des configurations intermédiaires valides (comme certains nœuds sans frères, ce qui est correct dans la plupart des cas) à moins que vous ne souhaitiez pas gérer des objets partiellement construits.
Par exemple, une arête de graphe qui n'a pas encore de destination n'est pas encore une arête de graphe valide.
la source
Considérez quatre possibilités:
Pour moi, chacun des 2, 3 et 4 est adapté à une situation de différence. Le premier est difficile à aimer, pour les raisons citées par l'OP, et est généralement le symptôme d'un design qui a subi un certain fluage et a besoin d'être refactorisé.
Ce que j'énumère comme (2) est bon quand il n'y a pas d'état derrière «l'usine», alors que (3) est la conception de choix quand il y a un état. Je me retrouve à utiliser (2) plutôt que (3) lorsque je ne veux pas m'inquiéter des threads et de la synchronisation, et je n'ai pas à m'inquiéter d'amortir une configuration coûteuse sur la production de nombreux objets. (3), en revanche, est appelé quand un vrai travail entre dans la construction de l'usine (mise en place depuis un SPI, lecture des fichiers de configuration, etc.).
Enfin, la réponse de quelqu'un d'autre a mentionné l'option (4), où vous avez beaucoup de petits objets immuables et le modèle préférable est d'obtenir des nouveaux à partir d'anciens.
Notez que je ne suis pas membre du `` fan club de modèles '' - bien sûr, certaines choses valent la peine d'être imitées, mais il me semble qu'ils prennent une vie inutile une fois que les gens leur donnent des noms et des chapeaux amusants.
la source
Une autre option potentielle consiste à refactoriser pour avoir moins de champs configurables. Si des groupes de champs ne fonctionnent (principalement) qu'entre eux, rassemblez-les dans leur propre petit objet immuable. Les constructeurs / générateurs de ce "petit" objet devraient être plus faciles à gérer, tout comme le constructeur / constructeur de ce "gros" objet.
la source
J'utilise C #, et ce sont mes approches. Considérer:
Option 1. Constructeur avec paramètres optionnels
Utilisé comme par exemple
new Foo(5, b: new Bar(whatever))
. Pas pour les versions Java ou C # antérieures à 4.0. mais cela vaut la peine d'être montré, car c'est un exemple de la façon dont toutes les solutions ne sont pas indépendantes du langage.Option 2. Constructeur prenant un seul objet paramètre
Exemple d'utilisation:
C # à partir de 3.0 rend cela plus élégant avec la syntaxe d'initialisation d'objet (sémantiquement équivalente à l'exemple précédent):
Option 3:
Reconcevez votre classe pour ne pas avoir besoin d'un si grand nombre de paramètres. Vous pouvez diviser ses responsabilités en plusieurs classes. Ou transmettez les paramètres non pas au constructeur mais uniquement à des méthodes spécifiques, à la demande. Pas toujours viable, mais quand c'est le cas, ça vaut le coup.
la source