Explication sur la façon dont «Dites, ne demandez pas» est considéré comme un bon OO

49

Cet article a été publié dans Hacker News avec plusieurs votes positifs. Venant du C ++, la plupart de ces exemples semblent aller à l’encontre de ce que j’ai appris.

Tels que l'exemple n ° 2:

Mauvais:

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

contre bon:

system_monitor.check_for_overheating

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

Le conseil en C ++ est que vous devriez préférer les fonctions libres aux fonctions membres car elles augmentent l'encapsulation. Ces deux éléments sont sémantiquement identiques, alors pourquoi préférer le choix qui a accès à plus d’état?

Exemple 4:

Mauvais:

def street_name(user)
  if user.address
    user.address.street_name
  else
    'No street name on file'
  end
end

contre bon:

def street_name(user)
  user.address.street_name
end

class User
  def address
    @address || NullAddress.new
  end
end

class NullAddress
  def street_name
    'No street name on file'
  end
end

Pourquoi est-il de la responsabilité de Userformater une chaîne d'erreur sans rapport? Que faire si je veux faire autre chose que d’imprimer 'No street name on file's’il n’ya pas de rue? Et si la rue porte le même nom?


Est-ce que quelqu'un pourrait m'éclairer sur les avantages et la raison d'être du programme «Dites, ne demandez pas»? Je ne cherche pas ce qui est préférable, mais j'essaie plutôt de comprendre le point de vue de l'auteur.

Pubby
la source
Les exemples de code pourraient être Ruby et pas Python, je ne sais pas.
Pubby
2
Je me demande toujours si quelque chose comme le premier exemple n'est pas une violation du PÉR?
stijn
1
Vous pouvez lire que: pragprog.com/articles/tell-dont-ask
Mik378
Rubis. @ est un raccourci par exemple et Python termine ses blocs implicitement par des espaces.
Erik Reppen
3
"Le conseil en C ++ est que vous devriez préférer les fonctions libres aux fonctions membres, car elles augmentent l'encapsulation." Je ne sais pas qui vous a dit ça, mais ce n'est pas vrai. Les fonctions libres peuvent être utilisées pour augmenter l'encapsulation, mais elles n'augmentent pas nécessairement l'encapsulation.
Rob K

Réponses:

81

Interroger l'objet sur son état, puis appeler des méthodes sur cet objet en fonction de décisions prises en dehors de l'objet, signifie que l'objet est maintenant une abstraction qui fuit; une partie de son comportement est située à l' extérieur de l'objet et l'état interne est exposé (peut-être inutilement) au monde extérieur.

Vous devez vous efforcer de dire aux objets ce que vous voulez qu'ils fassent. ne leur posez pas de questions sur leur état, prenez une décision et dites-leur ensuite quoi faire.

Le problème est qu'en tant qu'appelant, vous ne devez pas prendre de décisions en fonction de l'état de l'objet appelé, ce qui vous oblige à en modifier l'état. La logique que vous implémentez est probablement la responsabilité de l'objet appelé, pas la vôtre. Prendre des décisions en dehors de l'objet enfreint son encapsulation.

Bien sûr, vous pouvez dire que c'est évident. Je n'écrirais jamais un code comme ça. Néanmoins, il est très facile de se laisser bercer en examinant un objet référencé, puis en appelant différentes méthodes en fonction des résultats. Mais ce n'est peut-être pas la meilleure façon de s'y prendre. Dites à l'objet ce que vous voulez. Laissez-le comprendre comment le faire. Pensez de manière déclarative plutôt que procédurale!

Il est plus facile de rester en dehors de ce piège si vous commencez par concevoir des classes en fonction de leurs responsabilités. vous pouvez ensuite passer naturellement à la spécification des commandes que la classe peut exécuter, par opposition aux requêtes qui vous informent de l’état de l’objet.

http://pragprog.com/articles/tell-dont-ask

Robert Harvey
la source
4
L'exemple de texte interdit beaucoup de choses qui sont clairement une bonne pratique.
DeadMG
13
@DeadMG fait ce que vous dites seulement à ceux qui le suivent servilement, qui ignorent aveuglément le mot "pragmatique" dans le nom du site et la pensée clé des auteurs de site qui a été clairement énoncé dans leur livre de clés: "la meilleure solution n'existe pas ." ... "
Gnat
2
Ne lis jamais le livre. Je ne voudrais pas non plus. Je ne lis que l'exemple, ce qui est tout à fait juste.
DeadMG
3
@DeadMG pas de soucis. Maintenant que vous connaissez le point clé qui place cet exemple (et tout autre exemple de pragprog) dans le contexte souhaité ("rien de tel que la meilleure solution ..."), il est correct de ne pas lire le livre
Gnat
1
Je ne suis toujours pas sûr de ce que Tell, Don't Ask est supposé vous expliquer sans contexte, mais c'est vraiment un bon conseil OOP.
Erik Reppen
16

En règle générale, l'article suggère que vous ne devriez pas exposer les États membres aux autres, si vous pouviez les raisonner vous-même .

Cependant, ce qui n’est pas clairement indiqué, c’est que cette loi se heurte à des limites très évidentes lorsque le raisonnement dépasse largement la responsabilité d’une classe donnée. Par exemple, chaque classe dont le travail consiste à conserver une valeur ou à en fournir une, en particulier les valeurs génériques, ou lorsque la classe fournit un comportement qui doit être étendu.

Par exemple, si le système fournit le temperaturecomme une requête, demain, le client peut check_for_underheatingsans avoir à changer SystemMonitor. Ce n’est pas le cas lorsque l’ SystemMonitorimplémentation check_for_overheatingelle-même. Ainsi, une SystemMonitorclasse dont le travail est de déclencher une alarme lorsque la température est trop élevée le suit, mais une SystemMonitorclasse dont le travail est de permettre à un autre morceau de code de lire la température afin de pouvoir contrôler, par exemple, TurboBoost ou quelque chose comme ça. , ne devrait pas.

Notez également que le deuxième exemple utilise inutilement l'Anti-pattern Null Object.

DeadMG
la source
19
"Objet nul" n'est pas ce que j'appellerais un anti-motif, alors je me demande quelle est votre raison de le faire?
Konrad Rudolph
4
Plutôt sûr que personne n’a de méthodes spécifiées comme "Ne fait rien". Cela rend leur appel un peu inutile. Cela signifie que tout objet implémentant Null Object rompt, à tout le moins, le LSP et se décrit comme implémentant des opérations qu’il ne fait pas. L'utilisateur attend une valeur en retour. La correction de leur programme en dépend. Vous apportez simplement plus de problèmes en prétendant que c'est une valeur quand ce n'est pas le cas. Avez-vous déjà essayé de déboguer des méthodes en échec silencieuses? C'est impossible et personne ne devrait jamais avoir à subir cela.
DeadMG
4
Je dirais que cela dépend entièrement du domaine du problème.
Konrad Rudolph
5
@DeadMG Je suis d' accord que l'exemple ci - dessus est une mauvaise utilisation du Null modèle d'objet, mais il est un mérite de l' utiliser. Quelques fois, j'ai utilisé une implémentation «no-op» de telle ou telle interface pour éviter le contrôle nul ou la présence de «null» dans le système.
Max
6
Je ne suis pas sûr de comprendre votre argument avec "le client peut check_for_underheatingsans avoir à changer SystemMonitor". En quoi le client est-il différent de SystemMonitorce moment-là? Ne dissipez-vous pas maintenant votre logique de surveillance sur plusieurs classes? Je ne vois pas non plus le problème avec une classe de moniteur qui fournit des informations de capteur à d'autres classes tout en réservant des fonctions d'alarme à elle-même. Le contrôleur de suralimentation doit contrôler l’alimentation sans avoir à s’inquiéter de la possibilité de déclencher une alarme si la température est trop élevée.
TMN
9

Le véritable problème de votre exemple de surchauffe est que les règles relatives à ce qui est qualifié de surchauffe ne sont pas facilement modifiables pour différents systèmes. Supposons que le système A soit tel que vous l'avez (temp> 100 surchauffe) mais le système B est plus délicat (temp> 93 surchauffe). Modifiez-vous votre fonction de contrôle pour vérifier le type de système, puis appliquez la valeur correcte?

if (system is a System_A and system_monitor.temp >100)
  system_monitor.sound_alarms
else if (system is a System_B and system_monitor.temp > 93)
  system_monitor.sound_alarms
end

Ou avez-vous chaque type de système définir sa capacité de chauffage?

MODIFIER:

system.check_for_overheating

class SystemA : System
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

class SystemB : System
  def check_for_overheating
    if temperature > 93
      sound_alarms
    end
  end
end

La première façon de rendre votre fonction de contrôle devient laide lorsque vous commencez à traiter avec plus de systèmes. Ce dernier permet de stabiliser la fonction de contrôle au fil du temps.

Matthew Flynn
la source
1
Pourquoi ne pas avoir chaque système s’enregistrer avec le moniteur. Pendant l'enregistrement, ils peuvent indiquer quand survient une surchauffe.
Martin York
@ LokiAstari - Vous pourriez le faire, mais vous pourriez alors vous retrouver dans un nouveau système également sensible à l'humidité ou à la pression atmosphérique. Le principe est d'extraire ce qui varie - dans ce cas, c'est la susceptibilité à la surchauffe
Matthew Flynn
1
C'est exactement pourquoi vous devriez avoir un modèle tell. Vous indiquez au système les conditions actuelles et vous informe si les conditions de travail ne sont pas normales. Ainsi, vous n’aurez jamais besoin de modifier SystemMoniter. C'est l'encapsulation pour vous.
Martin York
@LokiAstari - je pense que nous parlons de choses contradictoires - je cherchais vraiment à créer différents systèmes plutôt que différents moniteurs. Le fait est que le système doit savoir quand il se trouve dans un état qui déclenche une alarme, par opposition à une fonction de contrôleur externe. SystemA devrait avoir ses critères, SystemB devrait avoir les siens. Le contrôleur devrait simplement pouvoir demander (à intervalles réguliers) si le système fonctionne correctement ou non.
Matthew Flynn
6

Tout d’abord, j’ai le sentiment que je dois admettre que vous qualifiez les exemples de «mauvais» et de «bon». L'article utilise les termes "Pas si bon" et "Meilleur", je pense que ces termes ont été choisis pour une raison: ce sont des lignes directrices, et selon les circonstances, l'approche "Pas si bon" peut être appropriée, voire la seule solution.

Lorsque vous avez le choix, vous devriez préférer inclure toute fonctionnalité qui repose uniquement sur la classe dans la classe plutôt qu'en dehors de celle-ci. La raison en est l'encapsulation et le fait qu'il est plus facile de faire évoluer la classe au fil du temps. La classe fait également un meilleur travail de publicité pour ses capacités qu'un tas de fonctions gratuites.

Parfois, il faut dire, parce que la décision repose sur quelque chose d'extérieur à la classe ou simplement parce que c'est quelque chose que vous ne voulez pas que la plupart des utilisateurs de la classe fassent. Parfois, vous voulez savoir, car le comportement est contre-intuitif pour la classe et vous ne voulez pas dérouter la plupart des utilisateurs de la classe.

Par exemple, vous vous plaignez du fait que l'adresse postale renvoie un message d'erreur. Ce n'est pas le cas, elle fournit une valeur par défaut. Mais parfois, une valeur par défaut n'est pas appropriée. S'il s'agissait d'un État ou d'une ville, vous souhaiterez peut-être définir une valeur par défaut lors de l'attribution d'un enregistrement à un vendeur ou à un répondant, de manière à ce que tous les inconnus soient attribués à une personne spécifique. Par contre, si vous imprimiez des enveloppes, vous préféreriez peut-être une exception ou une protection qui vous évite de gaspiller du papier avec des lettres qui ne peuvent pas être livrées.

Il peut donc y avoir des cas où "Pas si bon" est la voie à suivre, mais en général, "Mieux" est, eh bien, meilleur.

jmoreno
la source
3

Anti-symétrie données / objets

Comme d'autres l'ont souligné, Tell-Dont-Ask est spécifiquement destiné aux cas où vous modifiez l'état de l'objet après avoir demandé (voir par exemple le texte de Pragprog posté ailleurs sur cette page). Ce n'est pas toujours le cas, par exemple, l'objet 'utilisateur' n'est pas modifié après avoir été invité à indiquer son adresse utilisateur. C'est donc discutable s'il s'agit d'un cas approprié pour appliquer Tell-Dont-Ask.

Tell-Dont-Ask se préoccupe de responsabilité, de ne pas extraire la logique d’une classe qui devrait y figurer à juste titre. Mais toute la logique qui traite des objets n'est pas nécessairement la logique de ces objets. Ceci suggère un niveau plus profond, même au-delà de Tell-Dont-Ask, et je voudrais ajouter une brève remarque à ce sujet.

En ce qui concerne la conception architecturale, vous souhaiterez peut-être utiliser des objets qui ne sont en réalité que des conteneurs de propriétés, voire immuables, puis exécuter diverses fonctions sur des collections de tels objets, en les évaluant, en les filtrant ou en les transformant plutôt qu'en leur envoyant des commandes plus le domaine de Tell-Dont-Ask).

La décision qui convient le mieux à votre problème dépend de si vous souhaitez disposer de données stables (les objets déclaratifs) mais de la modification / l'ajout du côté de la fonction. Ou si vous vous attendez à avoir un ensemble stable et limité de telles fonctions mais attendez-vous à plus de flux au niveau des objets, par exemple en ajoutant de nouveaux types. Dans le premier cas, vous préféreriez des fonctions libres, dans le second méthodes d'objet.

Bob Martin, dans son livre "Clean Code", appelle cela "l'Anti-Symétrie Données / Objets" (p.95ff), d'autres communautés pourraient l'appeler le " problème d'expression ".

ThomasH
la source
3

Ce paradigme est parfois appelé «Dis, ne demande pas» , ce qui signifie dire à l'objet quoi faire, ne demande pas son état; et parfois comme "Demandez, ne dites pas" , ce qui signifie demander à l'objet de faire quelque chose pour vous, ne lui dites pas quel devrait être son état. Quoi qu'il en soit, la meilleure pratique est la même: la manière dont un objet doit exécuter une action est son intérêt, pas celui de l'appelant. Les interfaces doivent éviter d'exposer leur état (par exemple via des accesseurs ou des propriétés publiques) et exposer plutôt des méthodes de type «faire» dont la mise en œuvre est opaque. D'autres ont couvert cela avec les liens vers les programmeurs pragmatiques.

Cette règle est liée à la règle permettant d'éviter le code "à double point" ou "à double flèche", souvent appelée "Ne parler qu'à des amis immédiats", qui indique que foo->getBar()->doSomething()c'est mauvais, utilisez plutôt foo->doSomething();un appel encapsulant les fonctionnalités de barre, et mis en œuvre simplement return bar->doSomething();- s’il fooest responsable de la gestion bar, alors laissez-le faire!

Nicholas Shanks
la source
1

En plus des autres bonnes réponses sur "Dites, ne demandez pas", quelques commentaires sur vos exemples spécifiques qui pourraient aider:

Le conseil en C ++ est que vous devriez préférer les fonctions libres aux fonctions membres car elles augmentent l'encapsulation. Ces deux éléments sont sémantiquement identiques, alors pourquoi préférer le choix qui a accès à plus d’état?

Ce choix n'a pas accès à plus d'état. Ils utilisent tous les deux la même quantité d’État pour faire leur travail, mais le «mauvais» exemple exige que l’État de classe soit public pour pouvoir faire son travail. De plus, le comportement de cette classe dans le "mauvais" exemple est étendu à la fonction libre, ce qui le rend plus difficile à trouver et plus difficile à refactoriser.

Pourquoi est-il de la responsabilité de l'utilisateur de formater une chaîne d'erreur sans rapport? Que se passe-t-il si je veux faire autre chose que d’imprimer «Pas de nom de rue dans le dossier» s’il n’ya pas de rue? Et si la rue porte le même nom?

Pourquoi est-ce la responsabilité de 'nom de rue' de faire à la fois 'obtenir le nom de la rue' et 'de fournir un message d'erreur'? Au moins dans la «bonne» version, chaque pièce a une responsabilité. Pourtant, ce n'est pas un bon exemple.

Telastyn
la source
2
Ce n'est pas vrai. Vous présumez que la vérification de la surchauffe est la seule chose saine à faire avec la température. Que se passe-t-il si la classe est destinée à être l'un des nombreux moniteurs de température et qu'un système doit prendre des mesures différentes selon plusieurs de leurs résultats, par exemple? Lorsque ce comportement peut être limité au comportement prédéfini d'une seule instance, alors c'est sûr. Sinon, cela ne peut évidemment pas s'appliquer.
DeadMG
Bien sûr, ou si le thermostat et les alarmes existaient dans différentes classes (comme il se doit).
Telastyn
1
@DeadMG: Le conseil général est de rendre les choses privées / protégées jusqu'à ce que vous ayez besoin d'y accéder. Bien que cet exemple particulier soit meh, cela ne remet pas en cause la pratique standard.
Guvante
Un exemple dans un article sur la pratique "meh" la conteste un peu. Si cette pratique est standard en raison de ses grands avantages, alors pourquoi la difficulté de trouver un exemple approprié?
Stijn de Witt
1

Ces réponses sont très bonnes, mais voici un autre exemple à souligner: notez que c'est généralement un moyen d'éviter les doubles emplois. Par exemple, supposons que vous ayez plusieurs endroits avec un code tel que:

Product product = productMgr.get(productUuid)
if (product.userUuid != currentUser.uuid) {
    throw BlahException("This product doesn't belong to this user")
}

Cela signifie que vous feriez mieux d'avoir quelque chose comme ça:

Product product = productMgr.get(productUuid, currentUser)

Parce que cette duplication signifie que la plupart des clients de votre interface utiliseraient la nouvelle méthode, au lieu de répéter la même logique ici et là. Vous donnez à votre délégué le travail que vous voulez faire, au lieu de demander les informations dont vous avez besoin pour le faire vous-même.

antonio.fornie
la source
0

Je crois que cela est plus vrai lors de l'écriture d'un objet de haut niveau, mais moins vrai lorsque vous descendez au niveau plus profond, par exemple une bibliothèque de classes, car il est impossible d'écrire chaque méthode pour satisfaire tous les consommateurs de classe.

Par exemple # 2, je pense que c'est trop simplifié. Si nous allions réellement implémenter cela, SystemMonitor finirait par avoir le code pour un accès matériel de bas niveau et une logique pour une abstraction de haut niveau intégrée dans la même classe. Malheureusement, si nous essayons de séparer cela en deux classes, nous violerions le "Dis, ne demande pas" lui-même.

L'exemple n ° 4 est plus ou moins identique: il intègre la logique de l'interface utilisateur au niveau des données. Maintenant, si nous voulons corriger ce que l'utilisateur veut voir en cas d'absence d'adresse, nous devons corriger l'objet dans la couche de données, et que se passe-t-il si deux projets utilisant le même objet mais doivent utiliser un texte différent pour une adresse nulle?

Je conviens que si nous pouvions mettre en œuvre «Dis, ne demande pas» pour tout, ce serait très utile - je serais moi-même heureux si je pouvais juste dire plutôt que de demander (et de le faire moi-même) dans la vraie vie! Toutefois, comme dans la vie réelle, la faisabilité de la solution est très limitée aux classes de haut niveau.

tia
la source