Est-il utile d'utiliser des générateurs et des interfaces fluides avec des initialiseurs d'objets?

10

En Java et C #, vous pouvez créer un objet avec des propriétés qui peuvent être définies lors de l'initialisation en définissant un constructeur avec des paramètres, en définissant chaque propriété après la construction de l'objet ou en utilisant le modèle d'interface générateur / fluide. Cependant, C # 3 a introduit les initialiseurs d'objets et de collections, ce qui signifie que le modèle de générateur était largement inutile. Dans un langage sans initialiseurs, on pourrait implémenter un constructeur puis l'utiliser comme ceci:

Vehicle v = new Vehicle.Builder()
                    .manufacturer("Toyota")
                    .model("Camry")
                    .year(1997)
                    .colour(CarColours.Red)
                    .addSpecialFeature(new Feature.CDPlayer())
                    .addSpecialFeature(new Feature.SeatWarmer(4))
                    .build();

Inversement, en C # on pourrait écrire:

var vehicle = new Vehicle {
                Manufacturer = "Toyota",
                Model = "Camry",
                Year = 1997,
                Colour = CarColours.Red,
                SpecialFeatures = new List<SpecialFeature> {
                    new Feature.CDPlayer(),
                    new Feature.SeatWarmer { Seats = 4 }
                }
              }

... éliminant le besoin d'un constructeur, comme le montre l'exemple précédent.

Sur la base de ces exemples, les générateurs sont-ils toujours utiles en C #, ou ont-ils été entièrement remplacés par les initialiseurs?

svbnet
la source
are builders usefulpourquoi je continue à voir ce genre de questions? Un générateur est un modèle de conception - bien sûr, il peut être exposé en tant que fonctionnalité de langage, mais en fin de compte, ce n'est qu'un modèle de conception. Un motif de conception ne peut pas être "faux". Il peut ou non répondre à votre cas d'utilisation, mais cela ne rend pas le modèle entier incorrect, juste son application. N'oubliez pas qu'à la fin de la journée, les modèles de conception résolvent des problèmes particuliers - si vous ne rencontrez pas le problème, alors pourquoi essayez-vous de le résoudre?
VLAZ
@vlaz Je ne dis pas que les constructeurs ont tort - en fait ma question demandait s'il y avait des cas d'utilisation pour les constructeurs étant donné que les initialiseurs sont une implémentation du cas le plus courant où vous utiliseriez des constructeurs. De toute évidence, les gens ont répondu qu'un constructeur est toujours utile pour définir des champs privés, ce qui à son tour a répondu à ma question.
svbnet
3
@vlaz Pour clarifier: je dis que les initialiseurs ont largement remplacé le modèle "traditionnel" de générateur / interface fluide d'écriture d'une classe de générateur interne, puis d'écriture de méthodes de définition de chaînage à l'intérieur de cette classe qui créent une nouvelle instance de cette classe parente. Je ne dis pas que les initialiseurs remplacent ce modèle de générateur spécifique; Je dis que les initialiseurs évitent aux développeurs d'avoir à implémenter ce modèle de générateur spécifique, à l' exception des cas d'utilisation mentionnés dans les réponses, ce qui ne rend pas le modèle de générateur inutile.
svbnet
5
@vlaz Je ne comprends pas votre inquiétude. "Vous disiez qu'un modèle de conception n'est pas utile" - non, Joe demandait si un modèle de conception était utile. En quoi est-ce une mauvaise, mauvaise ou mauvaise question? Pensez-vous que la réponse est évidente? Je ne pense pas que la réponse soit évidente; cela me semble une bonne question.
Tanner Swett du
2
@vlaz, les modèles de singleton et de localisateur de service sont incorrects. Les modèles de conception Ergo peuvent être erronés.
David Arno

Réponses:

12

Comme évoqué par @ user248215, le vrai problème est l'immuabilité. La raison pour laquelle vous utiliseriez un générateur en C # serait de maintenir l'explicitation d'un initialiseur sans avoir à exposer les propriétés réglables. Ce n'est pas une question d'encapsulation, c'est pourquoi j'ai écrit ma propre réponse. L'encapsulation est assez orthogonale car invoquer un setter n'implique pas ce que le setter fait réellement ni ne vous lie à son implémentation.

La prochaine version de C #, 8.0, est susceptible d'introduire un withmot - clé qui permettra aux objets immuables d'être initialisés de manière claire et concise sans avoir besoin d'écrire des constructeurs.

Une autre chose intéressante que vous pouvez faire avec les constructeurs, par opposition aux initialiseurs, est qu'ils peuvent entraîner différents types d'objets en fonction de la séquence de méthodes appelées.

Par exemple

value.Match()
    .Case((DateTime d) => Console.WriteLine($"{d: yyyy-mm-dd}"))
    .Case((double d) => Console.WriteLine(Math.Round(d, 4));
    // void

var str = value.Match()
    .Case((DateTime d) => $"{d: yyyy-mm-dd}")
    .Case((double d) => Math.Round(d, 4).ToString())
    .ResultOrDefault(string.Empty);
    // string

Pour clarifier l'exemple ci-dessus, il s'agit d'une bibliothèque de correspondance de modèles qui utilise le modèle de générateur pour créer une "correspondance" en spécifiant des cas. Les cas sont ajoutés en appelant la Caseméthode en lui passant une fonction. Si valueest affectable au type de paramètre de la fonction, il est appelé. Vous pouvez trouver le code source complet sur GitHub et, comme les commentaires XML sont difficiles à lire en texte brut, voici un lien vers la documentation construite SandCastle (voir la section Remarques )

Aluan Haddad
la source
Je ne vois pas comment cela ne pourrait pas être fait un initialiseur d'objet, en utilisant un IEnumerable<Func<T, TResult>>membre.
Caleth
@Caleth C'est en fait quelque chose que j'ai expérimenté, mais il y a des problèmes avec cette approche. Il ne permet pas les clauses conditionnelles (non affichées mais utilisées et démontrées dans la documentation liée) et il ne permet pas l'inférence de type de TResult. En fin de compte, l'inférence de type y était pour beaucoup. Je voulais aussi qu'elle "ressemble" à une structure de contrôle. De plus, je ne voulais pas utiliser d'initialisation car cela permettrait une mutation.
Aluan Haddad
12

Les initialiseurs d'objets nécessitent que les propriétés soient accessibles par le code appelant. Les constructeurs imbriqués peuvent accéder aux membres privés de la classe.

Si vous vouliez rendre Vehicleimmuable (en rendant tous les setters privés), alors le générateur imbriqué pourrait être utilisé pour définir les variables privées.

user248215
la source
0

Ils servent tous à des fins différentes !!!

Les constructeurs peuvent initialiser les champs marqués readonlyainsi que les membres privés et protégés. Cependant, vous êtes quelque peu limité dans ce que vous pouvez faire à l'intérieur d'un constructeur; vous devez éviter de passer thisà des méthodes externes, par exemple, et vous devez éviter d'appeler des membres virtuels, car ils peuvent s'exécuter dans le contexte d'une classe dérivée qui ne s'est pas encore construite. De plus, les constructeurs sont garantis pour fonctionner (à moins que l'appelant ne fasse quelque chose de très inhabituel), donc si vous définissez des champs dans le constructeur, le code dans le reste de la classe peut supposer que ces champs ne seront pas nuls.

Les initialiseurs fonctionnent après la construction. Ils vous permettent uniquement d'appeler des propriétés publiques; les champs privés et / ou en lecture seule ne peuvent pas être définis. Par convention, l'acte de définir une propriété doit être assez limité, par exemple, il doit être idempotent et avoir des effets secondaires limités.

Les méthodes de générateur sont des méthodes réelles et permettent donc plus d'un argument, et par convention peuvent avoir des effets secondaires, y compris la création d'objets. Bien qu'ils ne puissent pas définir de champs en lecture seule, ils peuvent à peu près faire autre chose. De plus, les méthodes peuvent être implémentées en tant que méthodes d'extension (comme presque toutes les fonctionnalités de LINQ).

John Wu
la source