Lorsque j'utilise un chaînage de méthodes, dois-je réutiliser l'objet ou en créer un?

37

Lorsque vous utilisez une méthode de chaînage comme:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

il peut y avoir deux approches:

  • Réutilisez le même objet, comme ceci:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • Créez un nouvel objet de type Carà chaque étape, comme ceci:

    public Car PaintedIn(Color color)
    {
        var car = new Car(this); // Clone the current object.
        car.Color = color; // Assign the values to the clone, not the original object.
        return car;
    }

Le premier est-il faux ou s'agit-il plutôt d'un choix personnel du développeur?


Je crois que sa première approche peut rapidement causer le code intuitif / trompeur. Exemple:

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

// Would `specificModel` car be yellow or of neutral color? How would you guess that if
// `yellowCar` were in a separate method called somewhere else in code?

Des pensées?

Arseni Mourzenko
la source
1
Quel est le problème avec var car = new Car(Brand.Ford, 12345, Color.Silver);?
James
12
@James telescopic constructeur, le modèle fluent peut aider à distinguer les paramètres facultatifs des paramètres obligatoires (s’ils sont des arguments de constructeur requis, s’ils ne sont pas facultatifs). Et le courant est plutôt agréable à lire.
NimChimpsky
8
@NimChimpsky ce qui est arrivé à un bon vieux (pour C #) propriétés, et un constructeur qui a les champs qui sont nécessaires - pas que je suis dynamitage API Fluent, je suis un grand fan , mais ils sont souvent galvaudé
Chris S
8
@ChrisS si vous comptez sur des setters (je viens de java), vous devez rendre vos objets mutables, ce que vous ne voudrez peut-être pas faire. Et vous obtenez également plus agréable intellitext lorsque vous utilisez couramment - nécessite moins de réflexion, l'idé construit presque votre objet pour vous.
NimChimpsky
1
@NimChimpsky yeh je peux voir à quel point la fluence est un grand bond en avant pour Java
Chris S

Réponses:

41

Je mettrais l' api couramment à sa propre classe "constructeur", séparée de l'objet qu'elle crée. De cette façon, si le client ne veut pas utiliser l’API fluide, vous pouvez toujours l’utiliser manuellement et il ne pollue pas l’objet du domaine (conformément au principe de la responsabilité unique). Dans ce cas, les éléments suivants seraient créés:

  • Car qui est l'objet de domaine
  • CarBuilder qui tient la API couramment

L'utilisation serait comme ceci:

var car = CarBuilder.BuildCar()
    .OfBrand(Brand.Ford)
    .OfModel(12345)
    .PaintedIn(Color.Silver)
    .Build();

La CarBuilderclasse ressemblerait à ceci (j'utilise la convention de nommage C # ici):

public class CarBuilder {

    private Car _car;

    /// Constructor
    public CarBuilder() {
        _car = new Car();
        SetDefaults();
    }

    private void SetDefaults() {
        this.OfBrand(Brand.Ford);
          // you can continue the chaining for 
          // other default values
    }

    /// Starts an instance of the car builder to 
    /// build a new car with default values.
    public static CarBuilder BuildCar() {
        return new CarBuilder();
    }

    /// Sets the brand
    public CarBuilder OfBrand(Brand brand) {
        _car.SetBrand(brand);
        return this;
    }

    // continue with OfModel(...), PaintedIn(...), and so on...
    // that returns "this" to allow method chaining

    /// Returns the built car
    public Car Build() {
        return _car;
    }

}

Notez que cette classe ne sera pas thread-safe (chaque thread aura besoin de sa propre instance CarBuilder). Notez également que, même si le langage api fluide est un concept vraiment génial, il est probablement excessif de créer des objets de domaine simples.

Cette offre est plus utile si vous créez une API pour quelque chose de beaucoup plus abstrait et que sa configuration et son exécution sont plus complexes. C'est pourquoi elle fonctionne parfaitement dans les tests unitaires et les infrastructures DI. Vous pouvez voir d'autres exemples dans la section Java de l'article wikipedia Fluent Interface avec les objets persistance, datation et fictif.


MODIFIER:

Comme indiqué dans les commentaires; vous pouvez faire de la classe Builder une classe interne statique (dans la voiture) et la voiture peut être rendue immuable. Cet exemple de laisser la voiture immuable semble un peu ridicule; mais dans un système plus complexe, où vous ne voulez absolument pas changer le contenu de l'objet construit, vous pouvez le faire.

Vous trouverez ci-dessous un exemple illustrant comment utiliser à la fois la classe interne statique et comment gérer une création d'objet immuable qu'elle génère:

// the class that represents the immutable object
public class ImmutableWriter {

    // immutable variables
    private int _times; private string _write;

    // the "complex" constructor
    public ImmutableWriter(int times, string write) {
        _times = times;
        _write = write;
    }

    public void Perform() {
        for (int i = 0; i < _times; i++) Console.Write(_write + " ");
    }

    // static inner builder of the immutable object
    protected static class ImmutableWriterBuilder {

        // the variables needed to construct the immutable object
        private int _ii = 0; private string _is = String.Empty;

        public void Times(int i) { _ii = i; }

        public void Write(string s) { _is = s; }

        // The stuff is all built here
        public ImmutableWriter Build() {
            return new ImmutableWriter(_ii, _is);
        }

    }

    // factory method to get the builder
    public static ImmutableWriterBuilder GetBuilder() {
        return new ImmutableWriterBuilder();
    }
}

L'utilisation serait la suivante:

var writer = ImmutableWriter
                .GetBuilder()
                .Write("peanut butter jelly time")
                .Times(2)
                .Build();

writer.Perform();
// console writes: peanut butter jelly time peanut butter jelly time 

Edit 2: Pete dans les commentaires a publié un article sur l’utilisation des générateurs avec les fonctions lambda dans le contexte de l’écriture de tests unitaires avec des objets de domaine complexes. C'est une alternative intéressante pour rendre le constructeur un peu plus expressif.

Dans le cas de CarBuildervous devez avoir cette méthode à la place:

public static Car Build(Action<CarBuilder> buildAction = null) {
    var carBuilder = new CarBuilder();
    if (buildAction != null) buildAction(carBuilder);
    return carBuilder._car;
}

Qui peut être utilisé comme ceci:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);
Spoike
la source
3
@Baqueta ceci est décrit java efficace java
NimChimpsky
6
@ Baqueta a demandé une lecture pour java dev, à mon humble avis.
NimChimpsky
3
IMHO est un avantage énorme, que vous pouvez utiliser ce modèle (si modifié de manière appropriée) pour empêcher les instances de l'objet en construction qui ne sont pas terminées de s'échapper du constructeur. Par exemple, vous pouvez vous assurer qu'il n'y aura pas de voiture avec une couleur indéfinie.
scarfridge
1
Hmm ... J'ai toujours appelé la méthode finale du modèle de construction build()(ou Build()), et non le nom du type qu'il crée ( Car()dans votre exemple). De même, si Carun objet est vraiment immuable (par exemple, tous ses champs le sont readonly), même le constructeur ne pourra pas le muter, de sorte que la Build()méthode devient responsable de la construction de la nouvelle instance. Une façon de faire est de n'avoir Carqu'un seul constructeur, qui prend un constructeur comme argument; alors la Build()méthode peut simplement return new Car(this);.
Daniel Pryden
1
J'ai blogué sur une approche différente pour créer des constructeurs basés sur lambdas. Le message a probablement besoin d'un peu d'édition. Mon contexte était principalement celui de la portée d'un test unitaire, mais il pourrait également s'appliquer à d'autres domaines, le cas échéant. On peut le trouver ici: petesdotnet.blogspot.com/2012/05/…
Pete
9

Ça dépend.

Votre voiture est-elle une entité ou un objet de valeur ? Si la voiture est une entité, l'identité de l'objet est importante. Vous devez donc renvoyer la même référence. Si l'objet est un objet de valeur, il doit être immuable, ce qui signifie que le seul moyen est de renvoyer une nouvelle instance à chaque fois.

Un exemple de ce dernier serait la classe DateTime dans .NET, qui est un objet de valeur.

var date1 = new DateTime(2012,1,1);
var date2 = date1.AddDays(1);
// date2 now refers to Jan 2., while date1 remains unchanged at Jan 1.

Cependant, si le modèle est une entité, j'aime bien la réponse de Spoike sur l'utilisation d'une classe de générateur pour construire votre objet. En d'autres termes, cet exemple que vous avez donné n'a de sens qu'à mon humble avis si la voiture est un objet de valeur.

Pete
la source
1
+1 pour la question 'Entité' vs 'Valeur'. C'est une question de savoir si votre classe est un type mutable ou immuable (cet objet doit-il être changé?), Et à vous de choisir, bien que cela affecte votre conception. Je ne m'attendrais généralement pas à ce que l'enchaînement de méthodes fonctionne sur un type mutable, à moins que la méthode ne renvoie un nouvel objet.
Casey Kuball,
6

Créez un générateur interne statique distinct.

Utilisez des arguments de constructeur normaux pour les paramètres requis. Et api couramment pour facultatif.

Ne créez pas d'objet lorsque vous définissez la couleur, sauf si vous renommez la méthode NewCarInColour ou quelque chose de similaire.

Je ferais quelque chose comme ceci avec la marque requise et le reste optionnel (c'est du java, mais le votre ressemble à du javascript, mais je suis sûr qu'ils sont interchangeables avec un peu de nit picking):

Car yellowMercedes = new Car.Builder(Brand.MercedesBenz).PaintedIn(Color.Yellow).create();

Car specificYellowModel =new Car.Builder(Brand.MercedesBenz).WithModel(99).PaintedIn(Color.Yellow).create();
NimChimpsky
la source
4

Le plus important est que quelle que soit la décision que vous choisissez, cela est clairement indiqué dans le nom de la méthode et / ou dans le commentaire.

Il n'y a pas de standard. Parfois, la méthode renvoie un nouvel objet (la plupart des méthodes String le font) ou renvoie cet objet à des fins de chaînage ou d'efficacité de la mémoire.

J'ai déjà conçu un objet vectoriel 3D et les deux méthodes ont été implémentées pour chaque opération mathématique. Pour l'instant la méthode de la balance:

Vector3D scaleLocal(float factor){
    this.x *= factor; 
    this.y *= factor; 
    this.z *= factor; 
    return this;
}

Vector3D scale(float factor){
    Vector3D that = new Vector3D(this); // clone this vector
    return that.scaleLocal(factor);
}
XGouchet
la source
3
+1 Très bon point. Je ne vois pas vraiment pourquoi cela a eu un vote négatif. Je noterai cependant que les noms que vous avez choisis ne sont pas très clairs. Je les appellerais scale(le mutateur) et scaledBy(le générateur).
back2dos
Bon point, les noms auraient pu être plus clairs. La dénomination suivait une convention d'autres classes de mathématiques que j'avais utilisées dans une bibliothèque. L'effet a également été mentionné dans les commentaires javadoc de la méthode afin d'éviter toute confusion.
XGouchet
3

Je vois ici quelques problèmes qui, à mon avis, pourraient prêter à confusion ... Votre première phrase de la question:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

Vous appelez un constructeur (nouveau) et une méthode create ... Une méthode create () serait presque toujours une méthode statique ou une méthode de générateur, et le compilateur devrait la détecter dans un avertissement ou une erreur pour vous en informer, soit Ainsi, cette syntaxe est soit fausse soit porte des noms terribles. Mais plus tard, vous n'utilisez pas les deux, alors regardons cela.

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

Encore une fois avec la création, mais pas avec un nouveau constructeur. Le fait est que je pense que vous recherchez plutôt une méthode copy (). Donc, si c'est le cas et qu'il ne s'agit que d'un pauvre nom, examinons une chose ... vous appelez mercedes.Paintedin (Color.Yellow) .Copy () - Il devrait être facile de regarder cela et de dire que c'est en train d'être "peint" 'avant d'être copié - juste un flux de logique normal, pour moi. Alors mettez la copie en premier.

var yellowCar = mercedes.Copy().PaintedIn(Color.Yellow)

pour moi, il est facile de voir que vous peignez la copie et fabriquiez votre voiture jaune.

Drake Clarris
la source
+1 pour signaler la dissonance entre new et Create ();
Joshua Drake
1

La première approche a l'inconvénient que vous avez mentionné, mais tant que vous expliquez clairement dans la documentation, tout codeur semi-compétent ne devrait pas avoir de problèmes. Tout le code de chaine de méthodes avec lequel j'ai personnellement travaillé a fonctionné de cette façon.

La deuxième approche a évidemment l’inconvénient d’être plus de travail. Vous devez également décider si les copies que vous retournez seront des copies superficielles ou profondes: la meilleure solution peut varier d’une classe à l’autre ou d’une méthode à l’autre. Vous allez donc introduire une incohérence ou compromettre le meilleur comportement. Il est à noter que c'est la seule option pour les objets immuables, comme les chaînes.

Quoi que vous fassiez, ne mélangez pas et ne faites pas correspondre dans la même classe!

vaughandroid
la source
1

Je préfère penser comme le mécanisme "Méthodes d'extension".

public Car PaintedIn(this Car car, Color color)
{
    car.Color = color;
    return car;
}
Amir Karimi
la source
0

Ceci est une variation des méthodes ci-dessus. Les différences sont qu'il existe des méthodes statiques sur la classe Car qui correspondent aux noms de méthode sur le générateur, vous n'avez donc pas besoin de créer explicitement un générateur:

Car car = Car.builder().ofBrand(Brand.Ford).ofColor("Green")...

Vous pouvez utiliser les mêmes noms de méthode que vous utilisez pour les appels de générateur chaînés:

Car car = Car.ofBrand(Brand.Ford).ofColor("Green")...

De plus, il existe une méthode .copy () sur la classe qui retourne un générateur rempli avec toutes les valeurs de l'instance actuelle. Vous pouvez donc créer une variante sur un thème:

Car red = car.copy().paintedIn("Red").build();

Enfin, la méthode .build () du générateur vérifie que toutes les valeurs requises ont été fournies et renvoie le cas échéant. Il serait peut-être préférable d'exiger certaines valeurs sur le constructeur du constructeur et de permettre au reste d'être optionnel; dans ce cas, vous voudriez l'un des modèles dans les autres réponses.

public enum Brand {
    Ford, Chrysler, GM, Honda, Toyota, Mercedes, BMW, Lexis, Tesla;
}

public class Car {
    private final Brand brand;
    private final int model;
    private final String color;

    public Car(Brand brand, int model, String color) {
        this.brand = brand;
        this.model = model;
        this.color = color;
    }

    public Brand getBrand() {
        return brand;
    }

    public int getModel() {
        return model;
    }

    public String getColor() {
        return color;
    }

    @Override public String toString() {
        return brand + " " + model + " " + color;
    }

    public Builder copy() {
        Builder builder = new Builder();
        builder.brand = brand;
        builder.model = model;
        builder.color = color;
        return builder;
    }

    public static Builder ofBrand(Brand brand) {
        Builder builder = new Builder();
        builder.brand = brand;
        return builder;
    }

    public static Builder ofModel(int model) {
        Builder builder = new Builder();
        builder.model = model;
        return builder;
    }

    public static Builder paintedIn(String color) {
        Builder builder = new Builder();
        builder.color = color;
        return builder;
    }

    public static class Builder {
        private Brand brand = null;
        private Integer model = null;
        private String color = null;

        public Builder ofBrand(Brand brand) {
            this.brand = brand;
            return this;
        }

        public Builder ofModel(int model) {
            this.model = model;
            return this;
        }

        public Builder paintedIn(String color) {
            this.color = color;
            return this;
        }

        public Car build() {
            if (brand == null) throw new IllegalArgumentException("no brand");
            if (model == null) throw new IllegalArgumentException("no model");
            if (color == null) throw new IllegalArgumentException("no color");
            return new Car(brand, model, color);
        }
    }
}
David Conrad
la source