Est-ce un anti-modèle si une propriété de classe crée et retourne une nouvelle instance d'une classe?


24

J'ai une classe appelée Headingqui fait quelques choses, mais elle devrait également être capable de retourner l'opposé de la valeur de titre actuelle, qui doit finalement être utilisée via la création d'une nouvelle instance de la Headingclasse elle-même.

Je peux avoir une simple propriété appelée reciprocalpour retourner l'en-tête opposé de la valeur actuelle, puis créer manuellement une nouvelle instance de la classe Heading, ou je peux créer une méthode comme createReciprocalHeading()pour créer automatiquement une nouvelle instance de la classe Heading et la renvoyer à la utilisateur.

Cependant, un de mes collègues m'a recommandé de simplement créer une propriété de classe appelée reciprocalqui renvoie une nouvelle instance de la classe elle-même via sa méthode getter.

Ma question est: n'est-ce pas un anti-modèle pour qu'une propriété d'une classe se comporte comme ça?

Je trouve cela particulièrement moins intuitif car:

  1. Dans mon esprit, une propriété d'une classe ne doit pas renvoyer une nouvelle instance d'une classe, et
  2. Le nom de la propriété, qui est reciprocal, n'aide pas le développeur à comprendre pleinement son comportement, sans obtenir l'aide de l'EDI ou vérifier la signature du getter.

Suis-je trop strict sur ce qu'une propriété de classe doit faire ou est-ce une préoccupation valable? J'ai toujours essayé de gérer l'état de la classe via ses champs et propriétés et son comportement via ses méthodes, et je ne vois pas comment cela correspond à la définition de la propriété de classe.


6
Comme le dit @Ewan dans le dernier paragraphe de sa réponse, loin d'être un anti-modèle, avoir Headingcomme type immuable et reciprocalrenvoyer un nouveau Headingest une bonne pratique "fosse de réussite". (avec la mise en garde lors des deux appels à reciprocaldevrait retourner "la même chose", c'est-à-dire qu'ils devraient passer le test d'égalité.)
David Arno

20
"ma classe n'est pas immuable". Il y a votre véritable anti-modèle, juste là. ;)
David Arno

3
@DavidArno Eh bien, parfois, il y a une énorme pénalité de performance pour rendre certaines choses immuables, surtout si le langage ne le prend pas en charge nativement, mais sinon je suis d'accord que l'immuabilité est une bonne pratique dans de nombreux cas.
53777A

2
@dcorking la classe est implémentée à la fois en C # et TypeScript mais je garde plutôt ce langage indépendant de la langue.
53777A

2
@Kaiserludi sonne comme une réponse (une excellente réponse) - les commentaires sont pour demander de la clarté
dcorking

Réponses:


22

Ce n'est pas inconnu d'avoir des choses comme Copy () ou Clone () mais oui je pense que vous avez raison de vous inquiéter pour celui-ci.

prends pour exemple:

h2 = h1.reciprocal
h2.Name = "hello world"
h1.reciprocal.Name = ?

Ce serait bien d'avoir un avertissement que la propriété était un nouvel objet à chaque fois.

vous pouvez également supposer que:

h2.reciprocal == h1

Toutefois. Si votre classe d'en-tête était un type de valeur immuable, vous seriez en mesure d'implémenter ces relations et la réciproque pourrait être un bon nom pour l'opération


1
Il suffit de noter que Copy()et Clone()sont des méthodes, pas de propriétés. Je pense que le PO fait référence aux getters qui retournent de nouvelles instances.
Lynn

6
je sais. mais les propriétés sont (en quelque sorte) aussi des méthodes en c #
Ewan

4
Dans ce cas, C # a beaucoup plus à offrir: String.Substring(), Array.Reverse(), Int32.GetHashCode(), etc.
Lynn

If your heading class was an immutable value type- Je pense que vous devez ajouter que les setters de propriétés sur les objets immuables doivent toujours renvoyer les nouvelles instances (ou du moins si la nouvelle valeur n'est pas la même que l'ancienne), car c'est la définition des objets immuables. :)
Ivan Kolmychek

17

Une interface d'une classe amène l'utilisateur de la classe à faire des hypothèses sur son fonctionnement.

Si bon nombre de ces hypothèses sont correctes et que quelques-unes sont fausses, l'interface est bonne.

Si beaucoup de ces hypothèses sont erronées et peu ont raison, l'interface est un déchet.

Une hypothèse courante sur les propriétés est que l'appel de la fonction get est bon marché. Une autre hypothèse courante sur les propriétés est que l'appel de la fonction get deux fois de suite renvoie la même chose.


Vous pouvez contourner ce problème en utilisant la cohérence, afin de changer les attentes. Par exemple, pour une petite bibliothèque 3D où vous avez besoin Vector, Ray Matrixetc., vous pouvez faire en sorte que les getters tels que Vector.normalet Matrix.inversesoient à peu près toujours chers. Malheureusement, même si vos interfaces utilisent constamment des propriétés coûteuses, les interfaces seront au mieux aussi intuitives que celles qui utilisent Vector.create_normal()et Matrix.create_inverse()- pourtant je ne connais aucun argument solide qui puisse être avancé selon lequel l'utilisation des propriétés crée une interface plus intuitive, même après avoir changé les attentes.


10

Les propriétés doivent retourner très rapidement, retourner la même valeur avec des appels répétés et obtenir leur valeur ne devrait avoir aucun effet secondaire. Ce que vous décrivez ici ne doit pas être implémenté en tant que propriété.

Votre intuition est globalement correcte. Une méthode d'usine ne renvoie pas la même valeur avec des appels répétés. Il renvoie une nouvelle instance à chaque fois. Cela le disqualifie à peu près en tant que propriété. Ce n'est pas non plus rapide si un développement ultérieur ajoute du poids à la création d'instance, par exemple les dépendances réseau.

Les propriétés doivent généralement être des opérations très simples qui récupèrent / définissent une partie de l'état actuel de la classe. Les opérations de type usine ne répondent pas à ces critères.


1
La DateTime.Nowpropriété est couramment utilisée et fait référence à un nouvel objet. Je pense que le nom d'une propriété peut aider à lever l'ambiguïté quant à savoir si le même objet est retourné à chaque fois. Tout est dans le nom et comment il est utilisé.
Greg Burghardt

3
DateTime.Now est considéré comme une erreur. Voir stackoverflow.com/questions/5437972/…
Brad Thomas

@GregBurghardt L'effet secondaire d'un nouvel objet peut ne pas être un problème en soi, car il DateTimes'agit d'un type de valeur et n'a pas de concept d'identité d'objet. Si je comprends bien, le problème est qu'il n'est pas déterministe et renvoie une nouvelle valeur à chaque fois.
Zev Spitz

1
@GregBurghardt DateTime.Newest statique, et vous ne pouvez donc pas vous attendre à ce qu'il représente un état d'un objet.
Tsahi Asher

5

Je ne pense pas qu'il y ait une réponse indépendante de la langue à cela, car ce qui constitue une «propriété» est une question spécifique à la langue, et ce qu'un appelant d'une «propriété» attend est également une question spécifique à la langue. Je pense que la façon la plus fructueuse d'y penser est de penser à quoi cela ressemble du point de vue de l'appelant.

En C #, les propriétés se distinguent en ce qu'elles sont (conventionnellement) capitalisées (comme les méthodes) mais manquent de parenthèses (comme les variables d'instances publiques). Si vous voyez le code suivant, sans documentation, à quoi vous attendez-vous?

var reciprocalHeading = myHeading.Reciprocal;

En tant que novice en C #, mais qui a lu les directives d'utilisation des propriétés de Microsoft , je m'attendrais Reciprocalà, entre autres:

  1. être un membre logique des données de la Headingclasse
  2. être peu coûteux à appeler, de sorte que je n'ai pas besoin de mettre en cache la valeur
  3. manque d'effets secondaires observables
  4. produire le même résultat si appelé deux fois de suite
  5. (peut-être) offrir un ReciprocalChangedévénement

Parmi ces hypothèses, (3) et (4) sont probablement correctes (en supposant qu'il Headings'agit d'un type de valeur immuable, comme dans la réponse d'Ewan ), (1) est discutable, (2) est inconnu mais également discutable, et (5) est peu susceptible de avoir un sens sémantique (bien que tout ce qui a un titre devrait peut-être avoir un HeadingChangedévénement). Cela me suggère que dans une API C #, «obtenir ou calculer l'inverse» ne devrait pas être implémenté en tant que propriété, mais surtout si le calcul est bon marché et Headingimmuable, c'est un cas limite.

(Notez, cependant, qu'aucune de ces préoccupations n'a quoi que ce soit à voir avec le fait d'appeler la propriété crée une nouvelle instance , pas même (2). La création d'objets dans le CLR, en soi, est assez bon marché.)

En Java, les propriétés sont une convention de dénomination de méthode. Si je vois

Heading reciprocalHeading = myHeading.getReciprocal();

mes attentes sont similaires à celles ci-dessus (si elles sont moins explicites): je m'attends à ce que l'appel soit bon marché, idempotent et dépourvu d'effets secondaires. Cependant, en dehors du cadre JavaBeans, le concept de «propriété» n'a pas beaucoup de sens en Java, et en particulier lorsque l'on considère une propriété immuable sans correspondant setReciprocal(), la getXXX()convention est maintenant quelque peu démodée. De Effective Java , deuxième édition (déjà plus de huit ans maintenant):

Les méthodes qui renvoient une non- booleanfonction ou un attribut de l'objet sur lequel elles sont invoquées sont généralement nommées avec un nom, une phrase nominale ou une expression verbale commençant par le verbe get…. Il existe un contingent vocal qui prétend que seule la troisième forme (en commençant par get) est acceptable, mais cette affirmation est peu fondée. Les deux premiers formulaires conduisent généralement à un code plus lisible… (p. 239)

Dans une API contemporaine plus fluide, je m'attendrais donc à voir

Heading reciprocalHeading = myHeading.reciprocal();

- ce qui suggérerait à nouveau que l'appel est bon marché, idempotent et manque d'effets secondaires, mais ne dirait rien si un nouveau calcul est effectué ou si un nouvel objet est créé. C'est bon; dans une bonne API, je m'en fiche.

En Ruby, une propriété n'existe pas. Il y a des «attributs», mais si je vois

reciprocalHeading = my_heading.reciprocal

Je n'ai aucun moyen immédiat de savoir si j'accède à une variable d'instance @reciprocalvia une attr_readerou une méthode d'accesseur simple, ou si j'appelle une méthode qui effectue un calcul coûteux. Le fait que le nom de la méthode soit un simple nom, bien que, plutôt que de dire calcReciprocal, suggère, encore une fois, que l'appel est au moins bon marché et n'a probablement pas d'effets secondaires.

Dans Scala, la convention de dénomination est que les méthodes avec effets secondaires prennent des parenthèses et les méthodes sans elles ne le font pas, mais

val reciprocal = heading.reciprocal

pourrait être:

// immutable public value initialized at creation time
val reciprocal: Heading = … 

// immutable public value initialized on first use
lazy val reciprocal: Heading = … 

// public method, probably recalculating on each invocation
def reciprocal: Heading = …

// as above, with parentheses that, by convention, the caller
// should only omit if they know the method has no side effects
def reciprocal(): Heading = …

(Notez que Scala autorise diverses choses qui sont néanmoins découragées par le guide de style . C'est l'une de mes principales contrariétés avec Scala.)

Le manque de parenthèses me dit que l'appel n'a pas d'effets secondaires; le nom, encore une fois, suggère que l'appel devrait être relativement bon marché. Au-delà de cela, je me fiche de savoir comment cela me donne de la valeur.

En bref: Connaissez la langue que vous utilisez et sachez quelles attentes les autres programmeurs apporteront à votre API. Tout le reste est un détail d'implémentation.


1
+1 une de mes réponses préférées, merci d'avoir pris le temps d'écrire ceci.
53777A

2

Comme d'autres l'ont dit, il est assez courant de renvoyer des instances de la même classe.

La dénomination doit aller de pair avec les conventions de dénomination de la langue.

Par exemple, en Java, je m'attendrais probablement à ce qu'il soit appelé getReciprocal();

Cela étant dit, je considérerais les objets mutables vs immuables .

Avec ceux immuables , les choses sont assez faciles, et cela ne fera pas de mal si vous retournez le même objet ou non.

Avec les mutables , cela peut devenir très effrayant.

b = a.reciprocal
a += 1
b = what?

Maintenant, à quoi fait référence b? L'inverse de la valeur d'origine d'un? Ou l'inverse du changé? Cela peut être un exemple trop court, mais vous obtenez le point.

Dans ces cas, recherchez un meilleur nom qui communique ce qui se passe, par exemple, createReciprocal()pourrait être un meilleur choix.

Mais cela dépend aussi du contexte.


Voulez-vous dire mutable ?
Carsten S,

@CarstenS Oui, bien sûr, merci. Je ne sais pas où était ma tête. :)
Eiko

Pas du tout d'accord getReciprocal(), car cela "sonne" comme un getter de style JavaBeans "normal". Préférez "createXXX ()" ou éventuellement "CalculateXXX ()" qui indiquent à d'autres programmeurs ou suggèrent qu'il se passe quelque chose de différent
user949300

@ user949300 Je suis un peu d' accord avec vos préoccupations - et j'ai mentionné le nom create ... plus bas. Mais il y a aussi des inconvénients. créer ... implique de nouvelles instances qui ne sont pas toujours le cas (pensez à des objets dédiés singuliers pour certains cas spéciaux), calculer ... sorte de détails de mise en œuvre des fuites - ce qui pourrait encourager de fausses hypothèses sur le prix.
Eiko

1

Dans l'intérêt de la responsabilité unique et de la clarté, j'aurais une ReverseHeadingFactory qui a pris l'objet et a renvoyé son revers. Cela rendrait plus clair qu'un nouvel objet était retourné et signifierait que le code pour produire l'inverse était encapsulé loin d'un autre code.


1
+1 pour la clarté du nom, mais quel est le problème avec SRP dans d'autres solutions?
53777A

3
Pourquoi recommandez-vous une classe d'usine plutôt qu'une méthode d'usine? (À mon avis, une responsabilité utile d'un objet est souvent d'émettre des objets qui sont comme lui, comme iterator.next. Cette légère violation de SRP cause-t-elle d'autres problèmes d'ingénierie logicielle?)
dcorking

2
@dcorking Une classe d'usine par rapport à une méthode (à mon avis) est généralement le même type de question que "L'objet est-il comparable, ou devrais-je faire un comparateur?" Il est toujours correct de faire un comparateur, mais seulement de les rendre comparables si c'est une façon assez universelle de les comparer. (Par exemple, les plus grands nombres sont plus grands que les plus petits, c'est bon pour les comparables, mais vous pourriez dire que tous les evens sont plus grands que toutes les cotes, je ferais cela comme un comparateur) - Donc pour cela, je pense qu'il n'y en a qu'un façon d'inverser un en-tête, donc je pencherais pour faire une méthode pour éviter une classe supplémentaire.
Captain Man

0

Ça dépend. Je m'attends généralement à ce que les propriétés retournent des choses qui font partie d'une instance, donc retourner une instance différente de la même classe serait un peu inhabituel.

Mais par exemple, une classe de chaîne pourrait avoir les propriétés "firstWord", "lastWord", ou si elle gère Unicode "firstLetter", "lastLetter" qui seraient des objets de chaîne complets - juste généralement plus petits.


0

Étant donné que les autres réponses couvrent la partie Propriété, je vais simplement répondre à votre # 2 sur reciprocal:

N'utilisez rien d'autre que reciprocal. C'est le seul terme correct pour ce que vous décrivez (et c'est le terme formel). N'écrivez pas de logiciels incorrects pour empêcher vos développeurs d'apprendre le domaine dans lequel ils travaillent. Dans la navigation, les termes ont des significations très spécifiques et utilisent quelque chose d'aussi inoffensif que cela reverseou oppositepourrait conduire à la confusion dans certains contextes.


0

Logiquement, non, ce n'est pas la propriété d'une rubrique. Si tel était le cas, vous pourriez également dire que le négatif d'un nombre est la propriété de ce nombre, et franchement, cela ferait perdre à la «propriété» tout son sens, presque tout serait une propriété.

Réciproque est une fonction pure d'un titre, de même que le négatif d'un nombre est une fonction pure de ce nombre.

En règle générale, si le définir n'a pas de sens, il ne devrait probablement pas être une propriété. Le paramétrer peut toujours être interdit bien sûr, mais l'ajout d'un setter devrait être théoriquement possible et significatif.

Maintenant, dans certaines langues, il pourrait être judicieux de l'avoir comme propriété comme spécifié dans le contexte de cette langue, si cela apporte des avantages en raison de la mécanique de cette langue. Par exemple, si vous souhaitez le stocker dans une base de données, il est probablement judicieux d'en faire une propriété s'il permet une gestion automatique par le cadre ORM. Ou peut-être que c'est un langage où il n'y a pas de distinction entre une propriété et une fonction membre sans paramètre (je ne connais pas un tel langage, mais je suis sûr qu'il y en a). Il appartient ensuite au concepteur d'API et au documenteur de faire la distinction entre les fonctions et les propriétés.


Edit: Pour C # en particulier, je regarderais les classes existantes, en particulier celles de la bibliothèque standard.

  • Il y a BigIntegeret des Complexstructures. Mais ce sont des structures, et n'ont que des fonctions statiques, et toutes les propriétés sont de type différent. Donc, pas beaucoup d'aide à la conception pour votre Headingclasse.
  • Math.NET Numerics a une Vectorclasse , qui a très peu de propriétés, et semble assez proche de la vôtre Heading. Si vous vous en tenez à cela, vous auriez alors une fonction réciproque, pas une propriété.

Vous voudrez peut-être regarder les bibliothèques réellement utilisées par votre code ou les classes similaires existantes. Essayez de rester cohérent, c'est généralement le meilleur, si une façon n'est pas clairement correcte et une autre mauvaise.


-1

Question très intéressante en effet. Je ne vois aucune violation de SOLID + DRY + KISS dans ce que vous proposez, mais, quand même, ça sent mauvais.

Une méthode qui renvoie une instance de la classe s'appelle un constructeur , non? vous créez donc une nouvelle instance à partir d'une méthode non constructeur. Ce n'est pas la fin du monde, mais en tant que client, je ne m'attendrais pas à ça. Cela se fait généralement dans des circonstances spécifiques: une usine ou, parfois, une méthode statique de la même classe (utile pour les singletons et pour ... les programmeurs peu orientés objet?)

Plus: que se passe-t-il si vous invoquez getReciprocal()l'objet retourné? vous obtenez une autre instance de la classe, qui pourrait très bien être une copie exacte de la première! Et si vous invoquez getReciprocal()celui-là? Des objets tentaculaires?

Encore une fois: et si l'invocateur a besoin de la valeur réciproque (comme scalaire, je veux dire)? getReciprocal()->getValue()? nous violons la loi de Déméter pour de petits gains.


J'accepterais volontiers les contre-arguments du downvoter, merci!
Dr Gianluigi Zane Zanettini

1
Je n'ai pas déçu, mais je soupçonne que la raison pourrait être le fait que cela n'ajoute pas vraiment grand-chose au reste des réponses, mais je peux me tromper.
53777A

2
SOLID et KISS sont souvent opposés l'un à l'autre.
whatsisname
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.