web-dev-qa-db-fra.com

Construire de gros objets immuables sans utiliser de constructeurs ayant de longues listes de paramètres

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.

Il 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. Une simple addSibling(S s) faciliterait tellement la création d'objet mais rend l'objet mutable.

Qu'utilisez-vous 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.

Solutions auxquelles je peux penser:

  1. "Abominations de constructeur avec de longues listes de paramètres"
  2. Le modèle Builder
96
Malax

Eh bien, vous voulez à la fois un objet plus facile à lire et immuable une fois créé?

Je pense qu'une interface fluide CORRECTEMENT FAIT vous aiderait.

Cela ressemblerait à ceci (exemple purement composé):

final Foo immutable = FooFactory.create()
    .whereRangeConstraintsAre(100,300)
    .withColor(Color.BLUE)
    .withArea(234)
    .withInterspacing(12)
    .build();

J'ai écrit "CORRECTEMENT FAIT" en gras parce que la plupart des programmeurs Java ont des interfaces fluides 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 (d'où votre Foo peut être immuable).

FooFactory.create () , oùXXX (..) et avecXXX (..) tous créent "autre chose".

Que quelque chose d'autre puisse être une FooFactory, voici une façon de le faire ....

Votre FooFactory ressemblerait à ceci:

// Notice the private FooFactory constructor
private FooFactory() {
}

public static FooFactory create() {
    return new FooFactory();
}

public FooFactory withColor( final Color col ) {
    this.color = color;
    return this;
}

public Foo build() {
    return new FooImpl( color, and, all, the, other, parameters, go, here );
}
75
SyntaxT3rr0r

Dans Scala 2.8, vous pouvez utiliser les paramètres nommés et par défaut ainsi que la méthode copy sur une classe de cas. Voici un exemple de code:

case class Person(name: String, age: Int, children: List[Person] = List()) {
  def addChild(p: Person) = copy(children = p :: this.children)
}

val parent = Person(name = "Bob", age = 55)
  .addChild(Person("Lisa", 23))
  .addChild(Person("Peter", 16))
60
Martin Odersky

Eh bien, considérez ceci sur Scala 2.8:

case class Person(name: String, 
                  married: Boolean = false, 
                  espouse: Option[String] = None, 
                  children: Set[String] = Set.empty) {
  def marriedTo(whom: String) = this.copy(married = true, espouse = Some(whom))
  def addChild(whom: String) = this.copy(children = children + whom)
}

scala> Person("Joseph").marriedTo("Mary").addChild("Jesus")
res1: Person = Person(Joseph,true,Some(Mary),Set(Jesus))

Cela a bien sûr son lot de problèmes. Par exemple, essayez de créer espouse et Option[Person], puis de marier deux personnes. Je ne peux pas penser à un moyen de résoudre cela sans recourir à un private var et/ou un constructeur private plus une fabrique.

20
Daniel C. Sobral

Voici quelques options supplémentaires:

Option 1

Rendez l'implémentation elle-même mutable, mais séparez les interfaces qu'elle expose aux mutables et immuables. Ceci est tiré de la conception de la bibliothèque Swing.

public interface Foo {
  X getX();
  Y getY();
}

public interface MutableFoo extends Foo {
  void setX(X x);
  void setY(Y y);
}

public class FooImpl implements MutableFoo {...}

public SomeClassThatUsesFoo {
  public Foo makeFoo(...) {
    MutableFoo ret = new MutableFoo...
    ret.setX(...);
    ret.setY(...);
    return ret; // As Foo, not MutableFoo
  }
}

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 .

11

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:

Immuabilité popsicle: c'est ce que j'appelle fantaisiste un léger affaiblissement de l'immuabilité une seule fois. On pouvait imaginer un objet ou un champ qui est resté mutable un petit moment lors de son initialisation, puis s'est "figé" à jamais. Ce type d'immuabilité est particulièrement utile pour les objets immuables qui se référencent de façon circulaire, ou les objets immuables qui ont été sérialisés sur le disque et lors de la désérialisation doivent être "fluides" jusqu'à ce que le processus de désérialisation soit terminé, auquel cas tous les objets peuvent être congelé.

Donc, vous initialisez votre objet, puis définissez un indicateur "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.

6
Juliet

Considérez quatre possibilités:

new Immutable(one, fish, two, fish, red, fish, blue, fish); /*1 */

params = new ImmutableParameters(); /*2 */
params.setType("fowl");
new Immutable(params);

factory = new ImmutableFactory(); /*3 */
factory.setType("fish");
factory.getInstance();

Immutable boringImmutable = new Immutable(); /* 4 */
Immutable lessBoring = boringImmutable.setType("vegetable");

Pour moi, chacun des 2, 3 et 4 est adapté à une situation différente. Le premier est difficile à aimer, pour les raisons citées par l'OP, et est généralement le symptôme d'une conception qui a subi un certain fluage et a besoin d'une refactorisation.

Ce que j'énumère comme (2) est bon quand il n'y a pas d'état derrière "l'usine", tandis 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 me soucier des threads et de la synchronisation, et je n'ai pas à me soucier d'amortir une configuration coûteuse sur la production de nombreux objets. (3), d'autre part, est appelé lorsque de vrais travaux entrent dans la construction de l'usine (installation à partir d'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 motifs '' - bien sûr, certaines choses valent la peine d'être émulées, mais il me semble qu'ils prennent leur propre vie inutile une fois que les gens leur donnent des noms et des chapeaux drôles.

5
bmargulies

Vous pouvez également faire en sorte que les objets immuables exposent des méthodes qui ressemblent à des mutateurs (comme addSibling) mais leur permettent de renvoyer une nouvelle instance. C'est ce que font les collections immuables Scala.

L'inconvénient est que vous pourriez 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 et sœurs, ce qui est correct dans la plupart des cas), sauf si vous ne voulez pas traiter des objets partiellement construits.

Par exemple, un Edge de graphique qui n'a pas encore de destination n'est pas un Edge de graphique valide.

5
ziggystar

Une autre option potentielle est de refactoriser pour avoir moins de champs configurables. Si des groupes de champs ne fonctionnent (principalement) que les uns avec les autres, regroupez-les dans leur propre petit objet immuable. Les constructeurs/constructeurs de ce "petit" objet devraient être plus faciles à gérer, tout comme le constructeur/constructeur de ce "grand" objet.

4
Carl

J'utilise C #, et ce sont mes approches. Considérer:

class Foo
{
    // private fields only to be written inside a constructor
    private readonly int i;
    private readonly string s;
    private readonly Bar b;

    // public getter properties
    public int I { get { return i; } }
    // etc.
}

Option 1. Constructeur avec paramètres optionnels

public Foo(int i = 0, string s = "bla", Bar b = null)
{
    this.i = i;
    this.s = s;
    this.b = b;
}

Utilisé par exemple new Foo(5, b: new Bar(whatever)). Pas pour Java ou versions C # avant 4.0. Mais 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 objet à paramètre unique

public Foo(FooParameters parameters)
{
    this.i = parameters.I;
    // etc.
}

class FooParameters
{
    // public properties with automatically generated private backing fields
    public int I { get; set; }
    public string S { get; set; }
    public Bar B { get; set; }

    // All properties are public, so we don't need a full constructor.
    // For convenience, you could include some commonly used initialization
    // patterns as additional constructors.
    public FooParameters() { }
}

Exemple d'utilisation:

FooParameters fp = new FooParameters();
fp.I = 5;
fp.S = "bla";
fp.B = new Bar();
Foo f = new Foo(fp);`

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):

FooParameters fp = new FooParameters { I = 5, S = "bla", B = new Bar() };
Foo f = new Foo(fp);

Option 3:
Reconcevoir 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 passez des 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.

2
Joren