À bien des égards, j'aime vraiment l'idée des interfaces Fluent, mais avec toutes les fonctionnalités modernes de C # (initialiseurs, lambdas, paramètres nommés), je me dis: "cela en vaut-il la peine?", Et "Est-ce le bon modèle pour utilisation?". Quelqu'un pourrait-il me donner, sinon une pratique acceptée, au moins sa propre expérience ou matrice de décision pour savoir quand utiliser le modèle Fluent?
Conclusion:
Quelques bonnes règles empiriques des réponses à ce jour:
Voici un exemple de ce que je veux dire par les fonctionnalités modernes qui le rendent moins nécessaire. Prenons par exemple une interface (peut-être un mauvais exemple) Fluent qui me permet de créer un employé comme:
Employees.CreateNew().WithFirstName("Peter")
.WithLastName("Gibbons")
.WithManager()
.WithFirstName("Bill")
.WithLastName("Lumbergh")
.WithTitle("Manager")
.WithDepartment("Y2K");
Pourrait facilement être écrit avec des initialiseurs comme:
Employees.Add(new Employee()
{
FirstName = "Peter",
LastName = "Gibbons",
Manager = new Employee()
{
FirstName = "Bill",
LastName = "Lumbergh",
Title = "Manager",
Department = "Y2K"
}
});
J'aurais également pu utiliser des paramètres nommés dans les constructeurs de cet exemple.
L'écriture d'une interface fluide (j'ai tâté avec elle) demande plus d'efforts, mais elle a un avantage car si vous le faites correctement, l'intention du code utilisateur résultant est plus évidente. C'est essentiellement une forme de langage spécifique au domaine.
En d'autres termes, si votre code est lu beaucoup plus qu'il n'est écrit (et quel code ne l'est pas?), Vous devriez envisager de créer une interface fluide.
Les interfaces fluides sont davantage axées sur le contexte et sont bien plus que de simples moyens de configurer des objets. Comme vous pouvez le voir dans le lien ci-dessus, j'ai utilisé une API fluide pour atteindre:
objectA.
alors intellisense vous donne plein d'indices. Dans mon cas ci-dessus, plm.Led.
vous donne toutes les options pour contrôler la LED intégrée, et plm.Network.
vous donne tout ce que vous pouvez faire avec l'interface réseau. plm.Network.X10.
vous donne le sous-ensemble d'actions réseau pour les appareils X10. Vous n'obtiendrez pas cela avec les initialiseurs de constructeur (sauf si vous voulez avoir à construire un objet pour chaque type d'action différent, ce qui n'est pas idiomatique).Une chose que je fais généralement est:
test.Property(t => t.SomeProperty)
.InitializedTo(string.Empty)
.CantBeNull() // tries to set to null and Asserts ArgumentNullException
.YaddaYadda();
Je ne vois pas comment vous pouvez aussi faire quelque chose comme ça sans une interface fluide.
Edit 2 : Vous pouvez également apporter des améliorations de lisibilité vraiment intéressantes, comme:
test.ListProperty(t => t.MyList)
.ShouldHave(18).Items()
.AndThenAfter(t => testAddingItemToList(t))
.ShouldHave(19).Items();
Scott Hanselman en parle dans Episode 260 de son podcast Hanselminutes avec Jonathan Carter. Ils expliquent qu'une interface fluide ressemble plus à une interface utilisateur sur une API. Vous ne devez pas fournir une interface fluide comme seul point d'accès, mais plutôt la fournir comme une sorte de code-UI au-dessus de "l'interface API régulière".
Jonathan Carter parle également un peu de la conception d'API sur son blog .
Les interfaces fluides sont des fonctionnalités très puissantes à fournir dans le contexte de votre code, lorsque vous utilisez le "bon" raisonnement.
Si votre objectif est de simplement créer d'énormes chaînes de code à une ligne comme une sorte de pseudo-boîte noire, alors vous aboyez probablement le mauvais arbre. Si, d'autre part, vous l'utilisez pour ajouter de la valeur à votre interface API en fournissant un moyen de chaîner les appels de méthode et d'améliorer la lisibilité du code, alors avec beaucoup de bonne planification et d'efforts, je pense que l'effort en vaut la peine.
J'éviterais de suivre ce qui semble devenir un "modèle" commun lors de la création d'interfaces fluentes, où vous nommez toutes vos méthodes fluides "avec" quelque chose, car cela prive une interface API potentiellement bonne de son contexte, et donc de sa valeur intrinsèque .
La clé est de considérer la syntaxe fluide comme une implémentation spécifique d'un langage spécifique au domaine. Comme un très bon exemple de ce dont je parle, jetez un œil à StoryQ, qui utilise la maîtrise comme moyen d'exprimer une DSL d'une manière très précieuse et flexible.
Note initiale: Je conteste une hypothèse dans la question et tire mes conclusions spécifiques (à la fin de ce post) à partir de ce. Parce que cela ne donne probablement pas une réponse complète et englobante, je marque ceci comme CW.
Employees.CreateNew().WithFirstName("Peter")…
Pourrait facilement être écrit avec des initialiseurs comme:
Employees.Add(new Employee() { FirstName = "Peter", … });
À mes yeux, ces deux versions devraient signifier et faire des choses différentes.
Contrairement à la version non fluide, la version fluide cache le fait que le nouveau Employee
est également Add
ed à la collection Employees
- il suggère seulement qu'un nouvel objet est Create
d.
La signification de ….WithX(…)
est ambiguë, en particulier pour les personnes provenant de F #, qui a un mot clé with
pour les expressions d'objet : Ils pourraient interpréter obj.WithX(x)
comme un nouveau objet dérivé de obj
identique à obj
à l'exception de sa propriété X
, dont la valeur est x
. En revanche, avec la deuxième version, il est clair qu'aucun objet dérivé n'est créé et que toutes les propriétés sont spécifiées pour l'objet d'origine.
….WithManager().With…
Ce ….With…
A encore une autre signification: passer du "focus" de l'initialisation de la propriété à un autre objet. Le fait que votre API courante ait deux significations différentes pour With
rend difficile l'interprétation correcte de ce qui se passe ... c'est peut-être pourquoi vous avez utilisé l'indentation dans votre exemple pour démontrer la signification voulue de ce code. Ce serait plus clair comme ceci:
(employee).WithManager(Managers.CreateNew().WithFirstName("Bill").…)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// value of the `Manager` property appears inside the parentheses,
// like with `WithName`, where the `Name` is also inside the parentheses
Conclusions: "Cacher" une fonctionnalité de langage assez simple, new T { X = x }
, Avec une API fluide (Ts.CreateNew().WithX(x)
) peut évidemment être fait, mais:
Il faut veiller à ce que les lecteurs du code fluide résultant comprennent toujours ce qu'il fait exactement. Autrement dit, l'API courante doit être transparente dans sa signification et sans ambiguïté. La conception d'une telle API peut demander plus de travail que prévu (il faudra peut-être en tester la facilité d'utilisation et l'acceptation), et/ou…
sa conception peut demander plus de travail qu'il n'est nécessaire: dans cet exemple, l'API courante ajoute très peu de "confort d'utilisation" à l'API sous-jacente (une fonctionnalité de langage). On pourrait dire qu'une API fluide devrait rendre la fonctionnalité d'API/langage sous-jacente "plus facile à utiliser"; c'est-à-dire que cela devrait économiser au programmeur un effort considérable. Si c'est juste une autre façon d'écrire la même chose, cela n'en vaut probablement pas la peine, car cela ne facilite pas la vie du programmeur, mais rend seulement le travail du concepteur plus difficile (voir conclusion # 1 ci-dessus).
Les deux points ci-dessus supposent en silence que l'API courante est une couche sur une API existante ou une fonctionnalité de langage. Cette hypothèse peut être une autre bonne ligne directrice: une API fluide peut être un moyen supplémentaire de faire quelque chose, pas le seul. Autrement dit, ce pourrait être une bonne idée d'offrir une API fluide comme choix "opt-in".
J'aime le style fluide, il exprime très clairement l'intention. Avec l'exemple d'initaliseur d'objet que vous avez après, vous devez avoir des setters de propriété publique pour utiliser cette syntaxe, vous ne le faites pas avec le style fluide. En disant cela, avec votre exemple, vous ne gagnez pas beaucoup sur les setters publics parce que vous avez presque opté pour un style de méthode set/get Java-esque.
Ce qui m'amène au deuxième point, je ne suis pas sûr si j'utiliserais le style fluide comme vous l'avez fait, avec beaucoup de setters de propriétés, j'utiliserais probablement la deuxième version pour ça, je le trouve mieux quand vous ont beaucoup de verbes à enchaîner, ou au moins beaucoup de choses plutôt que des paramètres.
Je ne connaissais pas le terme interface fluide , mais cela me rappelle quelques API que j'ai utilisées, notamment LINQ .
Personnellement, je ne vois pas comment les fonctionnalités modernes de C # empêcheraient l'utilité d'une telle approche. Je dirais plutôt qu'ils vont de pair. Par exemple. il est encore plus facile de réaliser une telle interface en utilisant méthodes d'extension .
Peut-être clarifiez votre réponse avec un exemple concret de la façon dont une interface fluide peut être remplacée en utilisant l'une des fonctionnalités modernes que vous avez mentionnées.