J'ai un problème de conception concernant les propriétés .NET.
interface IX
{
Guid Id { get; }
bool IsInvalidated { get; }
void Invalidate();
}
Problème:
Cette interface a deux propriétés en lecture seule, Id
et IsInvalidated
. Le fait qu'ils soient en lecture seule, cependant, n'est pas en soi une garantie que leurs valeurs resteront constantes.
Disons que j'avais l'intention de dire très clairement que…
Id
représente une valeur constante (qui peut donc être mise en cache en toute sécurité), tandis queIsInvalidated
peut changer sa valeur pendant la durée de vie d'unIX
objet (et ne doit donc pas être mis en cache).
Comment pourrais-je modifier interface IX
pour rendre ce contrat suffisamment explicite?
Mes trois tentatives de solution:
L'interface est déjà bien conçue. La présence d'une méthode appelée
Invalidate()
permet à un programmeur de déduire que la valeur de la propriété portant le même nomIsInvalidated
pourrait en être affectée.Cet argument ne s'applique que dans les cas où la méthode et la propriété sont nommées de manière similaire.
Augmentez cette interface avec un événement
IsInvalidatedChanged
:bool IsInvalidated { get; } event EventHandler IsInvalidatedChanged;
La présence d'un
…Changed
événement pourIsInvalidated
indique que cette propriété peut changer sa valeur, et l'absence d'un événement similaire pourId
est une promesse que cette propriété ne changera pas sa valeur.J'aime cette solution, mais c'est beaucoup de choses supplémentaires qui peuvent ne pas être utilisées du tout.
Remplacez la propriété
IsInvalidated
par une méthodeIsInvalidated()
:bool IsInvalidated();
Cela pourrait être un changement trop subtil. C'est censé être un indice qu'une valeur est calculée à chaque fois - ce qui ne serait pas nécessaire si c'était une constante. La rubrique MSDN "Choisir entre les propriétés et les méthodes" a ceci à dire à ce sujet:
Utilisez une méthode plutôt qu'une propriété dans les situations suivantes. […] L'opération renvoie un résultat différent à chaque appel, même si les paramètres ne changent pas.
À quel genre de réponses dois-je m'attendre?
Je suis plus intéressé par des solutions entièrement différentes au problème, avec une explication sur la façon dont elles ont battu mes tentatives ci-dessus.
Si mes tentatives sont logiquement viciées ou présentent des inconvénients importants non encore mentionnés, de sorte qu'il ne reste qu'une seule solution (ou aucune), je voudrais savoir où je me suis trompé.
Si les défauts sont mineurs et qu'il reste plus d'une solution après en avoir pris en considération, veuillez commenter.
À tout le moins, j'aimerais avoir des commentaires sur quelle est votre solution préférée et pour quelle (s) raison (s).
la source
IsInvalidated
besoin d'unprivate set
?class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
InvalidStateException
s, par exemple, mais je ne suis pas sûr que ce soit même théoriquement possible. Ce serait bien, cependant.Réponses:
Je préférerais la solution 3 plutôt que 1 et 2.
Mon problème avec la solution 1 est : que se passe-t-il s'il n'y a pas de
Invalidate
méthode? Supposons une interface avec uneIsValid
propriété qui renvoieDateTime.Now > MyExpirationDate
; vous pourriez ne pas avoir besoin d'uneSetInvalid
méthode explicite ici. Et si une méthode affecte plusieurs propriétés? Supposons un type de connexion avecIsOpen
etIsConnected
propriétés - les deux sont affectés par laClose
méthode.Solution 2 : Si le seul point de l'événement est d'informer les développeurs qu'une propriété portant le même nom peut renvoyer des valeurs différentes à chaque appel, alors je déconseille fortement. Vous devez garder vos interfaces courtes et claires; en outre, toutes les implémentations ne peuvent pas déclencher cet événement pour vous. Je vais réutiliser mon
IsValid
exemple ci-dessus: vous devez implémenter un Timer et déclencher un événement lorsque vous atteignezMyExpirationDate
. Après tout, si cet événement fait partie de votre interface publique, les utilisateurs de l'interface s'attendront à ce que cet événement fonctionne.Cela étant dit sur ces solutions: elles ne sont pas mauvaises. La présence d'une méthode ou d'un événement indiquera qu'une propriété portant le même nom peut renvoyer des valeurs différentes à chaque appel. Tout ce que je dis, c'est qu'ils ne suffisent pas à eux seuls pour transmettre ce sens.
La solution 3 est ce que j'irais. Comme l'a mentionné aviv, cela peut ne fonctionner que pour les développeurs C #. Pour moi, en tant que C # -dev, le fait qu'il
IsInvalidated
ne s'agisse pas d'une propriété transmet immédiatement le sens de "ce n'est pas seulement un simple accesseur, quelque chose se passe ici". Cependant, cela ne s'applique pas à tout le monde, et comme l'a souligné MainMa, le cadre .NET lui-même n'est pas cohérent ici.Si vous vouliez utiliser la solution 3, je recommanderais de la déclarer conventionnelle et de la faire suivre par toute l'équipe. La documentation est également essentielle, je crois. À mon avis, il est en fait assez simple d'indiquer des changements de valeurs: " retourne vrai si la valeur est toujours valide ", " indique si la connexion a déjà été ouverte " vs " retourne vrai si cet objet est valide ". Cela ne ferait pas de mal du tout d'être plus explicite dans la documentation.
Mon conseil serait donc:
Déclarez la solution 3 une convention et suivez-la régulièrement. Indiquez clairement dans la documentation des propriétés si une propriété a ou non des valeurs changeantes. Les développeurs travaillant avec des propriétés simples supposeront correctement qu'elles ne changent pas (c'est-à-dire qu'elles ne changent que lorsque l'état de l'objet est modifié). Les développeurs qui rencontrent une méthode qui sonne comme il pourrait être une propriété (
Count()
,Length()
,IsOpen()
) savent ce qui se passe et (espérons) someting lire la méthode docs pour comprendre exactement ce que fait la méthode et la façon dont il se comporte.la source
Il existe une quatrième solution: s'appuyer sur la documentation .
Comment savez-vous que la
string
classe est immuable? Vous le savez simplement, car vous avez lu la documentation MSDN.StringBuilder
, d'autre part, est mutable, car, encore une fois, la documentation vous le dit.Votre troisième solution n'est pas mauvaise, mais .NET Framework ne la suit pas . Par exemple:
sont des propriétés, mais ne vous attendez pas à ce qu'elles restent constantes à chaque fois.
Ce qui est systématiquement utilisé dans .NET Framework lui-même est le suivant:
est une méthode, tandis que:
est une propriété. Dans le premier cas, faire des choses peut nécessiter un travail supplémentaire, y compris, par exemple, interroger la base de données. Ce travail supplémentaire peut prendre un certain temps. Dans le cas d'une propriété, cela devrait prendre un peu de temps: en effet, la longueur peut changer pendant la durée de vie de la liste, mais il ne faut pratiquement rien pour la renvoyer.
Avec les classes, le simple fait de regarder le code montre clairement que la valeur d'une propriété ne changera pas pendant la durée de vie de l'objet. Par exemple,
est clair: le prix restera le même.
Malheureusement, vous ne pouvez pas appliquer le même modèle à une interface, et étant donné que C # n'est pas assez expressif dans ce cas, la documentation (y compris la documentation XML et les diagrammes UML) doit être utilisée pour combler l'écart.
la source
Lazy<T>.Value
calcul peut prendre très longtemps (lors de son premier accès).Mes 2 cents:
Option 3: En tant que non-C # -er, ce ne serait pas vraiment un indice; Cependant, à en juger par la citation que vous apportez, il devrait être clair pour les programmeurs C # compétents que cela signifie quelque chose. Vous devez cependant ajouter des documents à ce sujet.
Option 2: sauf si vous prévoyez d'ajouter des événements à tout ce qui peut changer, n'y allez pas.
Autres options:
id
, qui est généralement supposée immuable même dans les objets mutables.la source
Une autre option serait de diviser l'interface: en supposant qu'après avoir appelé
Invalidate()
chaque invocation deIsInvalidated
devrait retourner la même valeurtrue
, il ne semble y avoir aucune raison d'invoquerIsInvalidated
pour la même partie qui a invoquéInvalidate()
.Donc, je suggère, soit vous vérifiez l' invalidation, soit vous la provoquez , mais il semble déraisonnable de faire les deux. Par conséquent, il est logique d'offrir une interface contenant l'
Invalidate()
opération à la première partie et une autre interface pour vérifier (IsInvalidated
) à l'autre. Comme il est assez évident à partir de la signature de la première interface qu'elle entraînera l'invalidation de l'instance, la question restante (pour la deuxième interface) est en effet assez générale: comment spécifier si le type donné est immuable .Je sais, cela ne répond pas directement à la question, mais au moins cela la réduit à la question de savoir comment marquer un type immuable , ce qui est calme, un problème commun et compris. Par exemple, en supposant que tous les types sont mutables, sauf indication contraire, vous pouvez conclure que la deuxième interface (
IsInvalidated
) est mutable et peut donc modifier la valeur deIsInvalidated
temps en temps. Par contre, supposons que la première interface ressemble à ceci (pseudo-syntaxe):Puisqu'il est marqué immuable, vous savez que l'ID ne changera pas. Bien sûr, l'appel de invalidate entraînera la modification de l'état de l'instance, mais cette modification ne sera pas observable via cette interface.
la source
[Immutable]
tant qu'elle englobe des méthodes dont le seul but est de provoquer une mutation. Je déplacerais laInvalidate()
méthode vers l'Invalidatable
interface.Id
, vous obtiendrez la même valeur à chaque fois. La même chose s'applique àInvalidate()
(vous n'obtenez rien, car son type de retour estvoid
). Par conséquent, le type est immuable. Bien sûr, vous aurez des effets secondaires , mais vous ne pourrez pas les observer via l'interfaceIX
, ils n'auront donc aucun impact sur les clients deIX
.IX
c'est immuable. Et que ce sont des effets secondaires; ils sont simplement répartis entre deux interfaces divisées de telle sorte que les clients n'ont pas besoin de se soucier (dans le cas deIX
) ou qu'il est évident quel pourrait être l'effet secondaire (dans le cas deInvalidatable
). Mais pourquoi ne pas passerInvalidate
àInvalidatable
? Cela rendraitIX
immuable et pur, etInvalidatable
ne deviendrait pas plus mutable, ni plus impur (car ce sont déjà les deux).Invalidate()
etIsInvalidated
par le même consommateur de l'interface . Si vous appelezInvalidate()
, vous devez savoir qu'il est invalidé, alors pourquoi le vérifier? Ainsi, vous pouvez fournir deux interfaces distinctes à des fins différentes.