Quand une méthode d'une classe doit-elle retourner la même instance après s'être modifiée?

9

J'ai une classe qui a trois méthodes A(), B()et C(). Ces méthodes modifient la propre instance.

Bien que les méthodes doivent renvoyer une instance lorsque l'instance est une copie distincte (tout comme Clone()), j'ai le libre choix de retourner voidou la même instance ( return this;) lorsque je modifie la même instance dans la méthode et ne renvoie aucune autre valeur.

Lorsque je décide de retourner la même instance modifiée, je peux faire des chaînes de méthodes soignées comme obj.A().B().C();.

Serait-ce la seule raison de le faire?

Est-il même acceptable de modifier sa propre instance et de la renvoyer également? Ou doit-il seulement renvoyer une copie et laisser l'objet d'origine comme avant? Parce que lors du retour de la même instance modifiée, l'utilisateur supposerait peut-être que la valeur retournée est une copie, sinon elle ne serait pas retournée? Si ça va, quelle est la meilleure façon de clarifier ces choses sur la méthode?

Martin Braun
la source

Réponses:

7

Quand utiliser le chaînage

Le chaînage de fonctions est surtout populaire dans les langues où un IDE avec auto-complétion est courant. Par exemple, presque tous les développeurs C # utilisent Visual Studio. Par conséquent, si vous développez avec C #, l'ajout de chaînage à vos méthodes peut être un gain de temps pour les utilisateurs de cette classe, car Visual Studio vous aidera à créer la chaîne.

D'un autre côté, les langages comme PHP qui sont de nature très dynamique et qui ne prennent souvent pas en charge la saisie semi-automatique dans les IDE verront moins de classes qui prennent en charge le chaînage. Le chaînage ne sera approprié que lorsque des phpDocs corrects sont utilisés pour exposer les méthodes chaînables.

Qu'est-ce que le chaînage?

Étant donné une classe nommée, Fooles deux méthodes suivantes sont toutes deux chaînables.

function what() { return this; }
function when() { return new Foo(this); }

Le fait que l'on soit une référence à l'instance actuelle et que l'on crée une nouvelle instance ne change pas qu'il s'agit de méthodes chaînables.

Il n'y a pas de règle d'or selon laquelle une méthode chaînable doit uniquement référencer l'objet actuel. En fait, les méthodes chaînables peuvent être réparties sur deux classes différentes. Par exemple;

class B { function When() { return true; } };
class A { function What() { return new B(); } };

var a = new A();
var x = a.What().When();

Il n'y a aucune référence à thisdans aucun des exemples ci-dessus. Le code a.What().When()est un exemple de chaînage. Ce qui est intéressant, c'est que le type de classe Bn'est jamais affecté à une variable.

Une méthode est enchaînée lorsque sa valeur de retour est utilisée comme composant suivant d'une expression.

Voici un autre exemple

 // return value never assigned.
 myFile.Open("something.txt").Write("stuff").Close();

// two chains used in expression
int x = a.X().Y() * b.X().Y();

// a chain that creates new strings
string name = str.Substring(1,10).Trim().ToUpperCase();

Quand utiliser thisetnew(this)

Les cordes dans la plupart des langues sont immuables. Le chaînage d'appels de méthode entraîne donc toujours la création de nouvelles chaînes. Où un objet comme StringBuilder peut être modifié.

La cohérence est la meilleure pratique.

Si vous avez des méthodes qui modifient l'état d'un objet et retournent this, ne mélangez pas les méthodes qui retournent de nouvelles instances. Au lieu de cela, créez une méthode spécifique appelée Clone()qui le fera explicitement.

 var x  = a.Foo().Boo().Clone().Foo();

C'est beaucoup plus clair sur ce qui se passe à l'intérieur a.

L'étape de l'extérieur et du dos

J'appelle cela l' astuce aller-retour , car elle résout beaucoup de problèmes courants liés au chaînage. Cela signifie essentiellement que vous sortez de la classe d'origine pour entrer dans une nouvelle classe temporaire, puis revenez à la classe d'origine.

La classe temporaire existe uniquement pour fournir des fonctionnalités spéciales à la classe d'origine, mais uniquement dans des conditions spéciales.

Il y a souvent des moments où une chaîne doit changer d' état , mais la classe Ane peut pas représenter tous ces états possibles . Ainsi, au cours d'une chaîne, une nouvelle classe est introduite qui contient une référence à A. Cela permet au programmeur de passer à un état et de revenir à A.

Voici mon exemple, que l'état spécial soit appelé B.

class A {
    function Foo() { return this; }
    function Boo() { return this; }
    function Change() return new B(this); }
}

class B {
    var _a;
    function (A) { _a = A; }
    function What() { return this; }
    function When() { return this; }
    function End() { return _a; }
}

var a = new A();
a.Foo().Change().What().When().End().Boo();

Voilà un exemple très simple. Si vous vouliez avoir plus de contrôle, vous Bpourriez revenir à un nouveau super-type Aqui a différentes méthodes.

Reactgular
la source
4
Je ne comprends vraiment pas pourquoi vous recommandez le chaînage de méthodes en fonction uniquement de la prise en charge de la saisie semi-automatique de type Intellisense. Les chaînes de méthodes ou les interfaces fluides sont un modèle de conception d'API pour tout langage POO; la seule chose que la saisie semi-automatique empêche les fautes de frappe et les erreurs de type.
amon
@amon vous avez mal lu ce que j'ai dit. J'ai dit que c'est plus populaire lorsque l'intellisense est courant pour une langue. Je n'ai jamais dit que cela dépendait de la fonctionnalité.
Reactgular
3
Bien que cette réponse m'aide beaucoup à comprendre les possibilités de chaînage, je ne sais toujours pas quand utiliser le chaînage. Bien sûr, la cohérence est la meilleure . Cependant, je me réfère au cas où je modifie mon objet dans la fonction et return this. Comment puis-je aider les utilisateurs de mes bibliothèques à gérer et à comprendre cette situation? (Les autoriser à enchaîner les méthodes, même si ce n'est pas nécessaire, serait-ce vraiment correct? Ou devrais-je m'en tenir à une seule façon, exiger un changement?)
Martin Braun
@modiX si ma réponse est correcte, veuillez l'accepter comme réponse. Les autres éléments que vous recherchez sont des opinions sur la façon d'utiliser le chaînage, et il n'y a pas de bonne / mauvaise réponse à cela. C'est à vous de décider. Peut-être vous inquiétez-vous trop des petits détails?
Reactgular
3

Ces méthodes modifient la propre instance.

Selon la langue, avoir des méthodes qui retournent void/ unitet modifient leur instance ou leurs paramètres n'est pas idiomatique. Même dans les langages qui en faisaient plus (C #, C ++), il se démode avec le passage à une programmation de style plus fonctionnelle (objets immuables, fonctions pures). Mais supposons qu'il y ait une bonne raison pour l'instant.

Serait-ce la seule raison de le faire?

Pour certains comportements (pensez x++), on s'attend à ce que l'opération renvoie le résultat, même si elle modifie la variable. Mais c'est en grande partie la seule raison de le faire par vous-même.

Est-il même acceptable de modifier sa propre instance et de la renvoyer également? Ou doit-il seulement renvoyer une copie et laisser l'objet d'origine comme avant? Parce que lors du retour de la même instance modifiée, l'utilisateur admettrait peut-être que la valeur retournée est une copie, sinon elle ne serait pas retournée? Si ça va, quelle est la meilleure façon de clarifier ces choses sur la méthode?

Ça dépend.

Dans les langues où copy / new et return sont communs (C # LINQ et chaînes), renvoyer la même référence serait source de confusion. Dans les langues où la modification et le retour sont courants (certaines bibliothèques C ++), la copie serait source de confusion.

Rendre la signature sans ambiguïté (en retournant voidou en utilisant des constructions de langage comme des propriétés) serait la meilleure façon de clarifier. Après cela, clarifier le nom SetFoopour montrer que vous modifiez l'instance existante est bon. Mais la clé est de maintenir les idiomes de la langue / bibliothèque avec laquelle vous travaillez.

Telastyn
la source
Merci pour votre réponse. Je travaille principalement avec le framework .NET, et d'après votre réponse, il est plein de choses non idiomatiques. Alors que des choses comme les méthodes LINQ renvoient une nouvelle copie de l'original après avoir ajouté des opérations, etc., des choses comme Clear()ou Add()de tout type de collection modifieront la même instance et retourneront void. Dans les deux cas, vous dites que ce serait déroutant ...
Martin Braun
@modix - LINQ ne renvoie pas de copie lors de l'ajout d'opérations.
Telastyn
Pourquoi je peux faire obj = myCollection.Where(x => x.MyProperty > 0).OrderBy(x => x.MyProperty);alors? Where()renvoie un nouvel objet de collection. OrderBy()renvoie même un objet de collection différent, car le contenu est ordonné.
Martin Braun
1
@modix - Ils reviennent de nouveaux objets qui mettent en œuvre IEnumerable, mais pour être clair, ils ne sont pas des copies, et ils ne sont pas des collections (sauf ToList, ToArrayetc.).
Telastyn
C'est vrai, ce ne sont pas des copies, en plus ce sont de nouveaux objets, en effet. En disant également collection, je faisais référence à des objets sur lesquels vous pouvez compter (objets qui contiennent plus d'un objet dans un tableau quelconque), pas à des classes qui héritent de ICollection. Ma faute.
Martin Braun
0

(J'ai supposé C ++ comme langage de programmation)

Pour moi, c'est surtout un aspect de lisibilité. Si A, B, C sont des modificateurs, en particulier s'il s'agit d'un objet temporaire passé en paramètre à une fonction, par exemple

do_something(
      CPerson('name').age(24).height(160).weight(70)
      );

par rapport à

{
   CPerson person('name');
   person.set_age(24);
   person.set_height(160);
   person.set_weight(70);
   do_something(person);
}

Reclassement s'il est correct de renvoyer une référence à l'instance modifiée, je dirais oui, et je vous pointe par exemple vers les opérateurs de flux '>>' et '<<' ( http://www.cprogramming.com/tutorial/ operator_overloading.html )

AdrianI
la source
1
Vous vous êtes trompé, c'est C #. Cependant, je veux que ma question soit plus courante.
Martin Braun
Bien sûr, cela aurait pu être Java aussi, mais c'était un point mineur juste pour placer mon exemple avec les opérateurs dans leur contexte. L'essentiel était que cela se résume à la lisibilité, qui est influencée par les attentes et les antécédents du lecteur, qui eux-mêmes sont influencés par les pratiques habituelles dans le langage / les cadres particuliers dont il s'agit - comme les autres réponses l'ont également souligné.
AdrianI
0

Vous pouvez également faire le chaînage de méthode avec le retour de copie plutôt que de modifier le retour.

Un bon exemple en C # est string.Replace (a, b) qui ne change pas la chaîne sur laquelle il a été invoqué mais renvoie à la place une nouvelle chaîne vous permettant de chaîner joyeusement.

Peter Wone
la source
Oui je sais. Mais je ne voulais pas faire référence à ce cas, car je dois utiliser ce cas quand je ne veux pas modifier la propre instance, de toute façon. Cependant, merci de l'avoir signalé.
Martin Braun
Selon d'autres réponses, la cohérence de la sémantique est importante. Étant donné que vous pouvez être cohérent avec le retour de copie et que vous ne pouvez pas être cohérent avec le retour de modification (car vous ne pouvez pas le faire avec des objets immuables), je dirais que la réponse à votre question est de ne pas utiliser le retour de copie.
Peter Wone du