Beaucoup des plus languges de programmation populaires (tels que C ++, Java, Python , etc.) ont le concept de cacher / observation des variables ou des fonctions. Lorsque j'ai rencontré des problèmes de masquage ou d'observation, ils ont été la cause de bogues difficiles à trouver et je n'ai jamais vu de cas où j'ai trouvé nécessaire d'utiliser ces fonctionnalités des langues.
Il me semble qu'il serait préférable de ne pas cacher ni observer.
Quelqu'un connaît-il une bonne utilisation de ces concepts?
Mise à jour:
je ne fais pas référence à l'encapsulation des membres de la classe (membres privés / protégés).
Réponses:
Si vous interdisez le masquage et l'observation, vous disposez d'un langage dans lequel toutes les variables sont globales.
C'est clairement pire que d'autoriser des variables ou des fonctions locales qui peuvent masquer des variables ou des fonctions globales.
Si vous interdisez cacher et shadowing, et vous essayez de « protéger » certaines variables globales, vous créez une situation où le compilateur dit le programmeur « Je suis désolé, Dave, mais vous ne pouvez pas utiliser ce nom, il est déjà utilisé . " L'expérience avec COBOL montre que les programmeurs recourent presque immédiatement au blasphème dans cette situation.
Le problème fondamental n'est pas le masquage / l'observation, mais les variables globales.
la source
L'utilisation d'identificateurs précis et descriptifs est toujours une bonne utilisation.
Je pourrais faire valoir que le masquage de variables ne cause pas beaucoup de bogues, car avoir deux variables nommées de manière très similaire de types identiques / similaires (ce que vous feriez si le masquage de variables était interdit) est susceptible de provoquer autant de bogues et / ou bugs graves. Je ne sais pas si cet argument est correct , mais il est au moins plausiblement discutable.
L'utilisation d'une sorte de notation hongroise pour différencier les champs des variables locales contourne ce problème, mais a son propre impact sur la maintenance (et la santé mentale du programmeur).
Et (peut-être probablement la raison pour laquelle le concept est connu en premier lieu), il est beaucoup plus facile pour les langues de mettre en œuvre le masquage / l'observation que de le désactiver. Une implémentation plus facile signifie que les compilateurs sont moins susceptibles d'avoir des bogues. Une implémentation plus facile signifie que les compilateurs prennent moins de temps à écrire, ce qui entraîne une adoption plus rapide et plus large de la plate-forme.
la source
Juste pour nous assurer que nous sommes sur la même page, la méthode de "masquage" est lorsqu'une classe dérivée définit un membre du même nom que celui de la classe de base (qui, s'il s'agit d'une méthode / propriété, n'est pas marqué virtuel / remplaçable ), et lorsqu'il est appelé à partir d'une instance de la classe dérivée dans le "contexte dérivé", le membre dérivé est utilisé, tandis que s'il est appelé par la même instance dans le contexte de sa classe de base, le membre de la classe de base est utilisé. Ceci est différent de l'abstraction / remplacement de membre où le membre de la classe de base attend de la classe dérivée qu'elle définisse un remplacement, et des modificateurs de portée / visibilité qui "cachent" un membre aux consommateurs en dehors de la portée souhaitée.
La réponse courte à la raison pour laquelle cela est autorisé est que ne pas le faire forcerait les développeurs à violer plusieurs principes clés de la conception orientée objet.
Voici la réponse la plus longue; tout d'abord, considérez la structure de classe suivante dans un univers alternatif où C # n'autorise pas le masquage des membres:
Nous voulons décommenter le membre de Bar et, ce faisant, permettre à Bar de fournir une MyFooString différente. Cependant, nous ne pouvons pas le faire car cela violerait l'interdiction de la réalité alternative de cacher des membres. Cet exemple particulier serait plein de bogues et est un excellent exemple de pourquoi vous pourriez vouloir l'interdire; par exemple, quelle sortie de console obtiendriez-vous si vous faisiez ce qui suit?
Du haut de ma tête, je ne sais pas vraiment si vous obtiendrez "Foo" ou "Bar" sur cette dernière ligne. Vous obtiendrez certainement "Foo" pour la première ligne et "Bar" pour la seconde, même si les trois variables font référence exactement à la même instance avec exactement le même état.
Ainsi, les concepteurs du langage, dans notre univers alternatif, découragent ce code manifestement mauvais en empêchant le masquage des propriétés. Maintenant, en tant que codeur, vous avez vraiment besoin de faire exactement cela. Comment contournez-vous la limitation? Eh bien, une façon consiste à nommer la propriété de Bar différemment:
Parfaitement légal, mais ce n'est pas le comportement que nous voulons. Une instance de Bar produira toujours "Foo" pour la propriété MyFooString, quand nous voulions qu'elle produise "Bar". Non seulement nous devons savoir que notre IFoo est spécifiquement un bar, nous devons également savoir utiliser les différents accesseurs.
Nous pourrions également, de manière tout à fait plausible, oublier la relation parent-enfant et implémenter directement l'interface:
Pour cet exemple simple, c'est une réponse parfaite, tant que vous vous souciez seulement du fait que Foo et Bar sont tous deux IFoos. Le code d'utilisation de quelques exemples ne pourrait pas être compilé car un Bar n'est pas un Foo et ne peut pas être attribué en tant que tel. Cependant, si Foo avait une méthode utile "FooMethod" dont Bar avait besoin, vous ne pouvez plus hériter de cette méthode; vous devrez soit cloner son code dans Bar, soit faire preuve de créativité:
Il s'agit d'un hack évident, et bien que certaines implémentations des spécifications du langage OO ne représentent guère plus que cela, conceptuellement, c'est faux; si les consommateurs de besoin Bar d'exposer les fonctionnalités de Foo, Bar devrait être un Foo, pas avoir un Foo.
Évidemment, si nous contrôlions Foo, nous pouvons le rendre virtuel, puis le remplacer. Il s'agit de la meilleure pratique conceptuelle dans notre univers actuel lorsqu'un membre est censé être remplacé, et se maintiendrait dans tout autre univers qui ne permettait pas de se cacher:
Le problème avec cela est que l'accès aux membres virtuels est, sous le capot, relativement plus coûteux à effectuer, et donc vous ne voulez généralement le faire que lorsque vous en avez besoin. Le manque de masquage, cependant, vous oblige à être pessimiste quant aux membres qu'un autre codeur qui ne contrôle pas votre code source pourrait vouloir réimplémenter; la «meilleure pratique» pour toute classe non scellée serait de tout rendre virtuel, sauf si vous ne le vouliez pas spécifiquement. Il a également encore ne vous donne pas le comportement exact de sa cachette; la chaîne sera toujours "Bar" si l'instance est une barre. Parfois, il est vraiment utile de tirer parti des couches de données d'état cachées, en fonction du niveau d'héritage auquel vous travaillez.
En résumé, permettre aux membres de se cacher est le moindre de ces maux. Ne pas l'avoir entraînerait généralement de pires atrocités commises contre des principes orientés objet que de le permettre.
la source
IEnumerable
etIEnumerable<T>
, décrite dans le blog d' Eric Libbert sur le sujet, est un bon exemple d'utilisation réelle de la dissimulation de membres .Honnêtement, Eric Lippert, le développeur principal de l'équipe du compilateur C #, l' explique assez bien (merci Lescai Ionel pour le lien). Les .NET
IEnumerable
et lesIEnumerable<T>
interfaces sont de bons exemples de cas où le masquage de membres est utile.Au début de .NET, nous n'avions pas de génériques. L'
IEnumerable
interface ressemblait donc à ceci:Cette interface est ce qui nous a permis de
foreach
survoler une collection d'objets, mais nous avons dû transtyper tous ces objets pour les utiliser correctement.Viennent ensuite les génériques. Lorsque nous avons obtenu des génériques, nous avons également obtenu une nouvelle interface:
Maintenant, nous n'avons plus à lancer d'objets pendant que nous les parcourons! Woot! Maintenant, si le masquage des membres n'était pas autorisé, l'interface devrait ressembler à ceci:
Ce serait un peu bête, parce que
GetEnumerator()
etGetEnumeratorGeneric()
dans les deux cas faire à peu près exactement la même chose , mais ils ont légèrement différentes valeurs de retour. En fait, ils sont si similaires que vous souhaitez presque toujours utiliser la forme générique par défautGetEnumerator
, sauf si vous travaillez avec du code hérité qui a été écrit avant l'introduction des génériques dans .NET.Parfois , cacher membre ne permet plus de place pour le code méchant et des bugs difficiles à trouver. Cependant, il est parfois utile, par exemple lorsque vous souhaitez modifier un type de retour sans casser le code hérité. Ce n'est qu'une de ces décisions que les concepteurs de langage doivent prendre: gênons-nous les développeurs qui ont légitimement besoin de cette fonctionnalité et la laissons-nous ou incluons-nous cette fonctionnalité dans le langage et attrapons-nous les flaks de ceux qui sont victimes de son utilisation abusive?
la source
IEnumerable<T>.GetEnumerator()
masque leIEnumerable.GetEnumerator()
, c'est uniquement parce que C # n'a pas de types de retour covariants lors de la substitution. Logiquement, il s'agit d'une dérogation, entièrement conforme au LSP. Cacher, c'est quand vous avez une variable localemap
dans la fonction dans un fichier qui le faitusing namespace std
(en C ++).Votre question pourrait être lue de deux manières: soit vous posez des questions sur la portée des variables / fonctions en général, soit vous posez une question plus spécifique sur la portée dans une hiérarchie d'héritage. Vous n'avez pas mentionné spécifiquement l'héritage, mais vous avez mentionné des bogues difficiles à trouver, ce qui ressemble plus à la portée dans le contexte de l'héritage qu'à la portée ordinaire, donc je répondrai aux deux questions.
La portée en général est une bonne idée, car elle nous permet de concentrer notre attention sur une partie spécifique (espérons-le petite) du programme. Parce qu'il permet aux noms locaux de toujours gagner, si vous ne lisez que la partie du programme qui est dans une portée donnée, alors vous savez exactement quelles parties ont été définies localement et ce qui a été défini ailleurs. Soit le nom fait référence à quelque chose de local, auquel cas le code qui le définit est juste devant vous, soit c'est une référence à quelque chose en dehors de la portée locale. S'il n'y a pas de références non locales qui pourraient changer sous nous (en particulier les variables globales, qui pourraient être modifiées de n'importe où), alors nous pouvons évaluer si la partie du programme dans la portée locale est correcte ou non sans référence à n'importe quelle partie du reste du programme .
Cela peut parfois conduire à quelques bugs, mais cela compense largement en empêchant une énorme quantité de bugs autrement possibles. Autre que de faire une définition locale avec le même nom qu'une fonction de bibliothèque (ne faites pas ça), je ne vois pas un moyen facile d'introduire des bogues avec une portée locale, mais la portée locale est ce qui permet à de nombreuses parties du même programme d'utiliser i comme compteur d'index pour une boucle sans s'encombrer et laisse Fred descendre le couloir écrire une fonction qui utilise une chaîne nommée str qui n'encombrera pas votre chaîne avec le même nom.
J'ai trouvé un article intéressant de Bertrand Meyer qui traite de la surcharge dans le contexte de l'héritage. Il évoque une distinction intéressante, entre ce qu'il appelle la surcharge syntaxique (ce qui signifie qu'il y a deux choses différentes avec le même nom) et la surcharge sémantique (ce qui signifie qu'il y a deux implémentations différentes de la même idée abstraite). La surcharge sémantique serait bien, car vous vouliez l'implémenter différemment dans la sous-classe; une surcharge syntaxique serait la collision de noms accidentelle qui a causé un bogue.
La différence entre la surcharge dans une situation d'héritage qui est prévue et qui est un bogue est la sémantique (la signification), donc le compilateur n'a aucun moyen de savoir si ce que vous avez fait est bien ou mal. Dans une situation de portée simple, la bonne réponse est toujours la chose locale, de sorte que le compilateur peut déterminer quelle est la bonne chose.
La suggestion de Bertrand Meyer serait d'utiliser un langage comme Eiffel, qui n'autorise pas les conflits de noms comme celui-ci et force le programmeur à renommer l'un ou les deux, évitant ainsi complètement le problème. Ma suggestion serait d'éviter d'utiliser entièrement l'héritage, en évitant également complètement le problème. Si vous ne pouvez pas ou ne voulez pas faire l'une de ces choses, il y a encore des choses que vous pouvez faire pour réduire la probabilité d'avoir un problème avec l'héritage: suivez le LSP (Liskov Substitution Principle), préférez la composition à l'héritage, gardez vos hiérarchies d'héritage peu profondes et maintenez les classes dans une hiérarchie d'héritage petites. En outre, certaines langues peuvent émettre un avertissement, même si elles ne génèrent pas d'erreur, comme le ferait une langue comme Eiffel.
la source
Voici mes deux cents.
Les programmes peuvent être structurés en blocs (fonctions, procédures) qui sont des unités autonomes de logique de programme. Chaque bloc peut faire référence à des "choses" (variables, fonctions, procédures) en utilisant des noms / identifiants. Ce mappage des noms aux choses est appelé liaison .
Les noms utilisés par un bloc se répartissent en trois catégories:
Considérons par exemple le programme C suivant
La fonction
print_double_int
a un nom local (variable locale)d
et un argumentn
, et utilise le nom global externeprintf
, qui est dans la portée mais n'est pas défini localement.Notez que cela
printf
pourrait également être passé en argument:Normalement, un argument est utilisé pour spécifier les paramètres d'entrée / sortie d'une fonction (procédure, bloc), tandis que les noms globaux sont utilisés pour faire référence à des choses comme les fonctions de bibliothèque qui "existent dans l'environnement", et il est donc plus pratique de les mentionner seulement quand ils sont nécessaires. L'utilisation d'arguments au lieu de noms globaux est l'idée principale de l' injection de dépendances , qui est utilisée lorsque les dépendances doivent être rendues explicites au lieu d'être résolues en regardant le contexte.
Une autre utilisation similaire de noms définis en externe peut être trouvée dans les fermetures. Dans ce cas, un nom défini dans le contexte lexical d'un bloc peut être utilisé dans le bloc, et la valeur liée à ce nom continuera (typiquement) d'exister tant que le bloc s'y réfère.
Prenons par exemple ce code Scala:
La valeur de retour de la fonction
createMultiplier
est la fermeture(m: Int) => m * n
, qui contient l'argumentm
et le nom externen
. Le nomn
est résolu en regardant le contexte dans lequel la fermeture est définie: le nom est lié à l'argumentn
de fonctioncreateMultiplier
. Notez que cette liaison est créée lorsque la fermeture est créée, c'est-à-dire lorsqu'ellecreateMultiplier
est invoquée. Le nomn
est donc lié à la valeur réelle d'un argument pour une invocation particulière de la fonction. Comparez cela avec le cas d'une fonction de bibliothèque commeprintf
, qui est résolue par l'éditeur de liens lorsque l'exécutable du programme est construit.En résumé, il peut être utile de faire référence à des noms externes dans un bloc de code local afin que vous
L'observation intervient lorsque vous considérez que dans un bloc, vous n'êtes intéressé que par les noms pertinents définis dans l'environnement, par exemple dans la
printf
fonction que vous souhaitez utiliser. Si par hasard vous souhaitez utiliser un nom local (getc
,putc
,scanf
, ...) qui a déjà été utilisé dans l'environnement, vous voulez simplement d'ignorer (ombre) le nom global. Donc, quand vous pensez localement, vous ne voulez pas considérer le contexte entier (peut-être très grand).Dans l'autre sens, en pensant globalement, vous voulez ignorer les détails internes des contextes locaux (encapsulation). Par conséquent, vous devez observer, sinon l'ajout d'un nom global pourrait casser tous les blocs locaux qui utilisaient déjà ce nom.
En bout de ligne, si vous voulez qu'un bloc de code fasse référence à des liaisons définies en externe, vous devez observer pour protéger les noms locaux des noms globaux.
la source