Principe du moindre étonnement (POLA) et interfaces

17

Il y a un bon quart de siècle, lorsque j'apprenais le C ++, on m'a enseigné que les interfaces doivent être indulgentes et, dans la mesure du possible, ne pas se soucier de l'ordre dans lequel les méthodes ont été appelées car le consommateur peut ne pas avoir accès à la source ou à la documentation au lieu de cette.

Cependant, chaque fois que j'ai encadré des programmeurs juniors et des développeurs seniors qui m'ont entendu, ils ont réagi avec étonnement, ce qui m'a amené à me demander si c'était vraiment une chose ou si elle venait juste de sortir de la mode.

Aussi clair que de la boue?

Considérez une interface avec ces méthodes (pour créer des fichiers de données):

OpenFile
SetHeaderString
WriteDataLine
SetTrailerString
CloseFile

Maintenant, vous pouvez bien sûr les parcourir dans l'ordre, mais dites que vous ne vous souciez pas du nom du fichier (pensez a.out) ou de l'en-tête et de la chaîne de fin qui étaient inclus, vous pouvez simplement appeler AddDataLine.

Un exemple moins extrême pourrait être l'omission des en-têtes et des remorques.

Une autre encore pourrait être de définir les chaînes d'en-tête et de fin avant l'ouverture du fichier.

Est-ce un principe de conception d'interface qui est reconnu ou tout simplement la manière POLA avant qu'on lui donne un nom?

NB ne vous embourbez pas dans les détails de cette interface, c'est juste un exemple pour cette question.

Robbie Dee
la source
10
Le principe du "moindre étonnement" est beaucoup plus répandu dans la conception de l' interface utilisateur que dans la conception de "l'interface du programmeur d'application". La raison en est qu'on ne peut pas attendre d'un utilisateur d'un site Web ou d'un programme qu'il lise des instructions avant de l'utiliser, tandis qu'un programmeur est censé, au moins en principe, lire les documents de l'API avant de programmer avec eux.
Kilian Foth
7
@KilianFoth: Je suis sûr que Wikipedia a tort à ce sujet - POLA ne concerne pas seulement la conception d'interface utilisateur, le terme "principe de moindre surprise" (qui est tout à fait le même) est également utilisé par Bob Martin pour la conception de fonctions et de classes dans son Livre "Clean Code".
Doc Brown du
2
Souvent, une interface immuable est toujours meilleure. Vous pouvez spécifier toutes les données que vous souhaitez définir au moment de la construction. Pas d'ambiguïtés et la classe devient plus simple à écrire. (Parfois, ce schéma n'est pas possible, bien sûr.)
usr
4
Pas du tout d'accord sur le fait que POLA ne s'applique pas aux API. Cela s'applique à tout ce qu'un humain crée pour d'autres humains. Lorsque les choses agissent comme prévu, elles sont plus faciles à conceptualiser et créent donc une charge cognitive plus faible, permettant aux gens de faire plus de choses avec moins d'effort.
Gort the Robot

Réponses:

25

Une façon dont vous pouvez vous en tenir au principe du moindre étonnement est de considérer d'autres principes tels que les FAI et les SRP , ou même SEC .

Dans l'exemple spécifique que vous avez donné, la suggestion semble être qu'il existe une certaine dépendance de l'ordre de manipulation du fichier; mais votre API contrôle à la fois l'accès aux fichiers et le format des données, ce qui sent un peu comme une violation de SRP.

Edit / Update: cela suggère également que l'API elle-même demande à l'utilisateur de violer DRY, car il devra répéter les mêmes étapes chaque fois qu'il utilisera l'API .

Envisagez une autre API où les opérations d'E / S sont distinctes des opérations de données. et où l'API elle-même «possède» la commande:

ContentBuilder

SetHeader( ... )
AddLine( ... )
SetTrailer ( ... )

FileWriter

Open(filename) 
Write(content) throws InvalidContentException
Close()

Avec la séparation ci-dessus, le ContentBuilder n'a pas besoin de réellement "faire" autre chose que de stocker les lignes / en-tête / bande-annonce (Peut-être aussi une ContentBuilder.Serialize()méthode qui connaît l'ordre). En suivant d'autres principes SOLID, peu importe que vous définissiez l'en-tête ou la fin avant ou après l'ajout de lignes, car rien dans le ContentBuildern'est réellement écrit dans le fichier jusqu'à ce qu'il soit transmis à FileWriter.Write.

Il présente également l'avantage supplémentaire d'être un peu plus flexible; par exemple, il peut être utile d'écrire le contenu dans un enregistreur de diagnostics ou de le transmettre sur un réseau au lieu de l'écrire directement dans un fichier.

Lors de la conception d'une API, vous devez également prendre en compte le rapport d'erreurs, qu'il s'agisse d'un état, d'une valeur de retour, d'une exception, d'un rappel ou d'autre chose. L'utilisateur de l'API s'attendra probablement à pouvoir détecter par programme toute violation de ses contrats, ou même d'autres erreurs qu'il ne peut pas contrôler, telles que des erreurs d'E / S de fichiers.

Ben Cottrell
la source
Exactement ce que je cherchais - merci! Extrait de l'article du FAI: "(Le FAI) déclare qu'aucun client ne devrait être forcé de dépendre de méthodes qu'il n'utilise pas"
Robbie Dee
5
Ce n'est pas une mauvaise réponse, néanmoins le constructeur de contenu peut toujours être implémenté d'une manière où l'ordre des appels SetHeaderou AddLineimporte. Pour éliminer cette dépendance à l'ordre n'est ni FAI ni SRP, c'est simplement POLA.
Doc Brown
Lorsque l'ordre est important, vous pouvez toujours satisfaire POLA en définissant les opérations de telle sorte que l'exécution des étapes ultérieures nécessite une valeur renvoyée par les étapes précédentes, appliquant ainsi l'ordre avec le système de type. FileWriterpourrait alors exiger la valeur de la dernière ContentBuilderétape de la Writeméthode pour s'assurer que tout le contenu d'entrée est complet, ce qui rend InvalidContentExceptioninutile.
Dan Lyons
@DanLyons Je pense que c'est assez proche de la situation que le demandeur essaie d'éviter; où l' utilisateur de l'API doit connaître ou prendre soin de la commande. Idéalement, l'API elle-même devrait appliquer la commande, sinon elle pourrait potentiellement demander à l'utilisateur de violer DRY. C'est la raison de la séparation ContentBuilderet de permettre FileWriter.Writed'encapsuler ce peu de connaissances. L'exception serait nécessaire en cas de problème avec le contenu (par exemple, un en-tête manquant). Un retour pourrait également fonctionner, mais je ne suis pas fan de transformer les exceptions en codes retour.
Ben Cottrell
Mais cela vaut vraiment la peine d'ajouter plus de notes sur SEC et d'ordonner à la réponse.
Ben Cottrell
12

Il ne s'agit pas seulement de POLA, mais aussi d'empêcher un état invalide comme source possible de bogues.

Voyons comment nous pouvons fournir quelques contraintes à votre exemple sans fournir une implémentation concrète:

Première étape: ne laissez rien être appelé avant l'ouverture d'un fichier.

CreateDataFileInterface
  + OpenFile(filename : string) : DataFileInterface

DataFileInterface
  + SetHeaderString(header : string) : void
  + WriteDataLine(data : string) : void
  + SetTrailerString(trailer : string) : void
  + Close() : void

Maintenant, il devrait être évident qu'il CreateDataFileInterface.OpenFilefaut appeler pour récupérer une DataFileInterfaceinstance, où les données réelles peuvent être écrites.

Deuxième étape: assurez-vous que les en-têtes et les remorques sont toujours définis.

CreateDataFileInterface
  + OpenFile(filename : string, header: string, trailer : string) : DataFileInterface

DataFileInterface
  + WriteDataLine(data : string) : void
  + Close() : void

Vous devez maintenant fournir tous les paramètres requis à l'avance pour obtenir un DataFileInterfacenom de fichier, un en-tête et une bande-annonce. Si la chaîne de fin n'est pas disponible jusqu'à ce que toutes les lignes soient écrites, vous pouvez également déplacer ce paramètre vers Close()(renommer éventuellement la méthode en WriteTrailerAndClose()) afin que le fichier au moins ne puisse pas être terminé sans chaîne de fin.


Pour répondre au commentaire:

J'aime la séparation de l'interface. Mais je suis enclin à penser que votre suggestion concernant l'application (par exemple, WriteTrailerAndClose ()) frôle une violation de SRP. (C'est une chose avec laquelle j'ai eu du mal à plusieurs reprises, mais votre suggestion semble être un exemple possible.) Comment répondriez-vous?

Vrai. Je ne voulais pas me concentrer sur l'exemple plus que nécessaire pour faire valoir mon argument, mais c'est une bonne question. Dans ce cas, je pense que je l'appellerais Finalize(trailer)et dirais que cela ne fait pas trop. L'écriture de la bande-annonce et la fermeture ne sont que de simples détails de mise en œuvre. Mais si vous n'êtes pas d'accord ou avez une situation similaire où c'est différent, voici une solution possible:

CreateDataFileInterface
  + OpenFile(filename : string, header : string) : IncompleteDataFileInterface

IncompleteDataFileInterface
  + WriteDataLine(data : string) : void
  + FinalizeWithTrailer(trailer : string) : CompleteDataFileInterface

CompleteDataFileInterface
  + Close()

Je ne le ferais pas réellement pour cet exemple, mais cela montre comment appliquer la technique en conséquence.

Soit dit en passant, j'ai supposé que les méthodes doivent en fait être appelées dans cet ordre, par exemple pour écrire séquentiellement de nombreuses lignes. Si ce n'est pas obligatoire, je préférerais toujours un constructeur, comme le suggère Ben Cottrel .

Fabian Schmengler
la source
1
Vous êtes hélas tombé dans le piège que je vous avais explicitement conseillé d'éviter dès le départ. Un nom de fichier n'est pas requis - ni l'en-tête ni la bande-annonce. Mais le thème général de la division de l'interface est bon, alors +1 :-)
Robbie Dee
Oh, alors je vous ai mal compris, je pensais que cela décrivait l'intention de l'utilisateur, pas la mise en œuvre.
Fabian Schmengler
J'aime la séparation de l'interface. Mais je suis enclin à penser que votre suggestion concernant l'application (par exemple WriteTrailerAndClose()) frôle une violation de la SRP. (C'est une chose avec laquelle j'ai eu du mal à plusieurs reprises, mais votre suggestion semble être un exemple possible.) Comment répondriez-vous?
kmote
1
La réponse @kmote était trop longue pour un commentaire, voir ma mise à jour
Fabian Schmengler
1
Si le nom de fichier est facultatif, vous pouvez fournir une OpenFilesurcharge qui n'en nécessite pas.
5gon12eder