Quand faut-il parler couramment C #?

78

À bien des égards, j’aime beaucoup l’idée des interfaces Fluent, mais avec toutes les fonctionnalités modernes du C # (initialiseurs, lambdas, paramètres nommés), je me dis: "est-ce que ça vaut la peine?" Et "Est-ce le bon modèle pour utilisation?". Est-ce que n'importe qui pourrait me donner, sinon une pratique acceptée, au moins leur propre expérience ou matrice de décision pour quand utiliser le modèle Fluent?

Conclusion:

Quelques bonnes règles empiriques tirées des réponses à ce jour:

  • Les interfaces fluides aident beaucoup lorsque vous avez plus d'actions que de paramètres, car les appels bénéficient davantage de la transmission de contexte.
  • Les interfaces fluides doivent être considérées comme une couche au-dessus d'une interface API, et non comme le seul moyen d'utilisation.
  • Les fonctions modernes telles que les lambdas, les initialiseurs et les paramètres nommés peuvent fonctionner main dans la main pour rendre une interface fluide encore plus conviviale.

Voici un exemple de ce que je veux dire par les fonctionnalités modernes qui permettent de se sentir moins nécessaire. Prenons l'exemple d'une interface Fluent (peut-être médiocre) qui me permet de créer un employé tel que:

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 aussi pu utiliser des paramètres nommés dans les constructeurs de cet exemple.

Andrew Hanlon
la source
1
Bonne question, mais je pense que c'est plus une question de wiki
Ivo
Votre question est étiquetée "fluent-nhibernate". Alors essayez-vous de décider de créer une interface fluide ou d' utiliser la configuration nhibernate ou XML?
Ilya Kogan le
1
Voté pour migrer vers Programmers.SE
Matt Ellen
@ Ilya Kogan, je pense que c'est en fait une étiquette "interface fluide" qui est une étiquette générique pour le modèle d'interface fluide. Cette question ne concerne pas nhibernate, mais comme vous l'avez dit seulement s'il faut créer une interface fluide. Merci.
1
Ce billet m'a inspiré à réfléchir à une façon d'utiliser ce modèle en C. On peut trouver ma tentative sur le site soeur Code Review .
Octobre

Réponses:

28

Écrire une interface fluide (je me suis habitué à cela) demande plus d'effort, mais cela a des avantages, 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 à un 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 alors envisager de créer une interface fluide.

Les interfaces Fluent concernent davantage le contexte et constituent 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 couramment fluide pour réaliser:

  1. Contexte (ainsi, lorsque vous effectuez généralement plusieurs actions dans une séquence avec la même chose, vous pouvez les chaîner sans avoir à déclarer votre contexte à plusieurs reprises).
  2. La découvrabilité (quand vous allez à objectA.intellisense vous donne beaucoup d'indices. Dans mon cas, ci-dessus, plm.Led.vous donne toutes les options pour contrôler la DEL intégrée et plm.Network.vous donne les possibilités que vous pouvez utiliser avec l'interface réseau. plm.Network.X10.Vous donne le sous-ensemble de actions réseau pour les périphériques X10. Vous ne l'obtiendrez pas avec les initialiseurs de constructeur (à moins que vous ne souhaitiez avoir à construire un objet pour chaque type d'action différent, ce qui n'est pas idiomatique).
  3. Reflection (non utilisé dans l'exemple ci-dessus) - la capacité de prendre une expression passée dans LINQ et de la manipuler est un outil très puissant, en particulier dans certaines API d'aide I construites pour les tests unitaires. Je peux transmettre une expression de propriété getter, construire tout un tas d'expressions utiles, les compiler et les exécuter, ou même utiliser la propriété getter pour configurer mon contexte.

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 faire quelque chose comme ça aussi bien sans une interface fluide.

Edit 2 : Vous pouvez également apporter des améliorations de lisibilité vraiment intéressantes, comme par exemple:

test.ListProperty(t => t.MyList)
    .ShouldHave(18).Items()
    .AndThenAfter(t => testAddingItemToList(t))
    .ShouldHave(19).Items();
Scott Whitlock
la source
Merci pour la réponse, mais je suis conscient de la raison d'utiliser Fluent, mais je recherche une raison plus concrète de l'utiliser sur quelque chose comme mon nouvel exemple ci-dessus.
Andrew Hanlon
Merci pour la réponse prolongée. Je pense que vous avez défini deux bonnes règles empiriques: 1) Utilisez Fluent lorsque vous avez plusieurs appels qui bénéficient de la «transmission» du contexte. 2) Pensez à Fluent lorsque vous avez plus d'appels que de setters.
Andrew Hanlon
2
@ach, je ne vois rien dans cette réponse sur "plus d'appels que de setters". Etes-vous confus par sa déclaration à propos du "code [qui] est lu beaucoup plus qu'il n'est écrit"? Cela ne concerne pas les getters / setters de propriétés, mais les humains qui lisent le code et ceux qui écrivent le code. Il s'agit de rendre le code facile à lire pour les humains , car nous lisons généralement une ligne de code donnée beaucoup plus souvent que nous ne le modifions.
Joe White
@ Joe White, je devrais peut-être reformuler mon terme "appel" à "action". Mais l'idée est toujours valable. Merci.
Andrew Hanlon
La réflexion pour le test est diabolique!
Adronius le
24

Scott Hanselman en parle dans l' épisode 260 de son podcast Hanselminutes avec Jonathan Carter. Ils expliquent qu'une interface fluide ressemble davantage à une interface utilisateur sur une API. Vous ne devez pas fournir une interface fluide en tant que seul point d'accès, mais plutôt une interface utilisateur de code au-dessus de "l'interface API standard".

Jonathan Carter parle aussi un peu de la conception des API sur son blog .

Kristof Claes
la source
Merci beaucoup pour les liens d’information et l’interface utilisateur au-dessus de l’API est une bonne façon de voir les choses.
Andrew Hanlon
14

Les interfaces Fluent sont des fonctionnalités très puissantes à fournir dans le contexte de votre code, avec le raisonnement "correct".

Si votre objectif est simplement de créer d’énormes chaînes de codes d’une seule ligne comme une sorte de pseudo-boîte noire, vous êtes probablement en train d’aboyer le mauvais arbre. D'autre part, si vous l'utilisez pour ajouter de la valeur à votre interface API en fournissant un moyen d'enchaî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" courant lors de la création d'interfaces fluides, dans lesquelles 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é consiste à considérer la syntaxe fluide comme une implémentation spécifique d'un langage spécifique à un domaine. Pour un très bon exemple de ce dont je parle, jetez un coup d’œil à StoryQ, qui utilise la fluidité pour exprimer un DSL de manière très utile et flexible.

S.Robins
la source
Merci pour la réponse, il n'est jamais trop tard pour donner une réponse bien pensée.
Andrew Hanlon
Le préfixe "avec" pour les méthodes ne me dérange pas. Cela les distingue des autres méthodes qui ne renvoient pas d'objet à chaîner. Par exemple , position.withX(5)contreposition.getDistanceToOrigin()
LegendLength
5

Note initiale: je suis en désaccord avec une hypothèse de la question et j'en tire mes conclusions spécifiques (à la fin de ce post). Parce que cela ne donne probablement pas une réponse complète et globale, je le marque 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 doivent signifier et faire des choses différentes.

  • Contrairement à la version non-fluide, la version fluide cache le fait que la nouvelle Employeeest également Addliée à la collection Employees- elle suggère seulement qu'un nouvel objet est Created.

  • La signification de ….WithX(…)est ambiguë, en particulier pour les personnes issues de F #, qui possède un withmot - clé pour les expressions d'objet : elles peuvent être interprétées obj.WithX(x)comme un nouvel objet dérivé de objidentique à l' objexception de sa Xpropriété, dont la valeur est x. D'autre part, 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
  • Cela ….With…a encore un autre sens: changer le "focus" de l'initialisation de la propriété sur un autre objet. Le fait que votre API parle couramment a deux significations différentes, Withce qui rend difficile l'interprétation correcte de ce qui se passe… C'est peut-être pour cette raison que vous avez utilisé l'indentation dans votre exemple pour démontrer la signification de ce code. Ce serait plus clair comme ça:

    (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 un API courant ( Ts.CreateNew().WithX(x)) peut évidemment être fait, mais:

  1. Il faut veiller à ce que les lecteurs du code qui en résulte comprennent toujours ce qu’il fait exactement. C'est-à-dire que l'IPA fluide devrait avoir une signification transparente et sans ambiguïté. La conception d'une telle API peut nécessiter plus de travail que prévu (il devrait éventuellement être testé pour sa facilité d'utilisation et son acceptation), et / ou…

  2. la conception peut prendre plus de temps que nécessaire: dans cet exemple, l'API fluide ajoute très peu de "confort d'utilisation" par rapport à l'API sous-jacent (une fonctionnalité de langage). On pourrait dire qu'une API fluide devrait rendre la fonctionnalité API / langage sous-jacente "plus facile à utiliser"; c'est-à-dire que cela devrait économiser beaucoup d'effort au programmeur. S'il ne s'agit que d'une autre manière d'écrire la même chose, cela n'en vaut probablement pas la peine, car cela ne simplifie pas la vie du programmeur, mais rend le travail du concepteur plus difficile (voir la conclusion n ° 1 ci-dessus).

  3. Les deux points ci-dessus supposent que l'API fluide est une couche superposée à une API existante ou à une fonctionnalité de langage. Cette hypothèse est peut-être une autre bonne directive: une API fluide peut être un moyen supplémentaire de faire quelque chose, et non la seule. En d'autres termes, il peut être judicieux de proposer une API fluide en tant que choix "opt-in".

vitesse
la source
1
Merci d'avoir pris le temps d'ajouter à ma question. Je vais admettre que mon exemple choisi a été mal pensé. À l'époque, je cherchais en fait à utiliser une interface fluide pour une API d'interrogation que je développais. J'ai trop simplifié. Merci d’avoir signalé les erreurs et les bons points de conclusion.
Andrew Hanlon
2

J'aime le style fluide, il exprime l'intention très clairement. Avec l'exemple d'initiateur d'objet que vous avez après, vous devez disposer de personnes qui définissent les propriétés publiques pour utiliser cette syntaxe, mais pas avec le style fluide. En disant cela, avec votre exemple, vous ne gagnez pas grand-chose au public 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 couramment utilisé, avec de nombreux régleurs de propriété. J'utiliserais probablement la deuxième version pour cela. Je le trouve mieux lorsque vous avoir beaucoup de verbes à chaîner ensemble, ou au moins beaucoup de choses plutôt que de paramètres.

Ian
la source
Merci pour votre réponse, je pense que vous avez exprimé une bonne règle de base: la fluence est meilleure avec de nombreux appels sur plusieurs setters.
Andrew Hanlon le
1

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 en quoi 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 des méthodes d'extension .

Clarifiez peut-être 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.

Steven Jeuris
la source
1
Merci pour la réponse - j'ai ajouté un exemple de base pour clarifier ma question.
Andrew Hanlon