Faut-il privilégier la cohérence par rapport à la convention de programmation?

14

Lors de la conception d'une classe, la cohérence des comportements doit-elle être privilégiée par rapport aux pratiques de programmation courantes? Pour donner un exemple précis:

Une convention courante est la suivante: si une classe possède un objet (par exemple, il l'a créé), elle est responsable de le nettoyer une fois qu'il est terminé. Un exemple spécifique serait dans .NET que si votre classe possède un IDisposableobjet, elle devrait le disposer à la fin de sa vie. Et si vous ne le possédez pas, ne le touchez pas.

Maintenant, si nous regardons la StreamWriterclasse dans .NET, nous pouvons trouver dans la documentation qu'elle ferme le flux sous-jacent lorsqu'elle est fermée / supprimée. Cela est nécessaire dans les cas où l' StreamWriterinstanciation en passant dans un nom de fichier en tant que scénariste crée le flux de fichiers sous - jacent et doit donc fermer. Cependant, on peut également passer dans un flux externe que l'écrivain ferme également.

Cela m'a énervé des tas de fois (oui, je sais que vous pouvez créer un wrapper non fermant, mais ce n'est pas le sujet), mais apparemment, Microsoft a décidé qu'il est plus cohérent de toujours fermer le flux, peu importe d'où il vient.

Lorsque je rencontre un tel modèle dans l'une de mes classes, je crée généralement un ownsFooBarindicateur qui est défini sur false dans les cas où il FooBarest injecté via le constructeur et sur true dans le cas contraire. De cette façon, la responsabilité de le nettoyer est transférée à l'appelant lorsqu'il passe explicitement l'instance.

Maintenant, je me demande si peut-être la cohérence devrait être en faveur des meilleures pratiques (ou peut-être que mes meilleures pratiques ne sont pas si bonnes)? Des arguments pour / contre?

Modifier pour clarification

Par «cohérence», je veux dire: le comportement cohérent de la classe prenant toujours la propriété (et fermant le flux) par rapport aux «meilleures pratiques» pour ne s'approprier un objet que si vous l'avez créé ou transféré explicitement la propriété.

Quant à un exemple où c'est énervant:

Supposons que vous ayez deux classes données (provenant d'une bibliothèque tierce) qui acceptent un flux pour en faire quelque chose, comme créer et traiter des données:

 public class DataProcessor
 {
     public Result ProcessData(Stream input)
     {
          using (var reader = new StreamReader(input))
          {
              ...
          }
     }
 }

 public class DataSource
 {
     public void GetData(Stream output)
     {
          using (var writer = new StreamWriter(output))
          {
               ....
          }
     }
 }

Maintenant, je veux l'utiliser comme ceci:

 Result ProcessSomething(DataSource source)
 {
      var processor = new DataProcessor();
      ...
      var ms = new MemoryStream();
      source.GetData(ms);
      return processor.ProcessData(ms);
 }

Cela échouera avec une exception Cannot access a closed streamdans le processeur de données. C'est un peu construit mais devrait illustrer le point. Il existe différentes façons de le réparer, mais j'ai néanmoins l'impression de contourner quelque chose que je ne devrais pas avoir à faire.

ChrisWue
la source
Pourriez-vous expliquer pourquoi le comportement illustré du StreamWriter vous fait mal? Souhaitez-vous consommer le même flux ailleurs? Pourquoi?
Job
@Job: J'ai clarifié ma question
ChrisWue

Réponses:

9

J'utilise aussi cette technique, je l'appelle «transfert», et je crois qu'elle mérite le statut d'un motif. Lorsqu'un objet A accepte un objet jetable B comme paramètre de temps de construction, il accepte également un booléen appelé `` transfert '' (qui par défaut est faux), et s'il est vrai, la suppression de A se répercute en cascade sur la suppression de B.

Je ne suis pas en faveur de la prolifération des choix malheureux des autres, donc je n'accepterais jamais la mauvaise pratique de Microsoft comme une convention établie, et je ne considérerais pas les autres aveugles suivant les choix malheureux de Microsoft comme un "comportement cohérent" de quelque manière, forme ou forme .

Mike Nakis
la source
Remarque: J'ai écrit une définition formelle de ce modèle dans un article sur mon blog: michael.gr: le modèle "Handoff" .
Mike Nakis
Comme extension supplémentaire du handOffmodèle, si une telle propriété est définie dans le constructeur, mais qu'une autre méthode existe pour la modifier, cette autre méthode devrait également accepter un handOffindicateur. S'il a handOffété spécifié pour l'élément actuellement détenu, il doit être éliminé, puis le nouvel élément doit être accepté et l' handOffindicateur interne mis à jour pour refléter l'état du nouvel élément. La méthode ne devrait pas avoir besoin de vérifier les anciennes et les nouvelles références pour leur égalité, car si la référence d'origine a été "remise", toutes les références détenues par l'appelant d'origine doivent être abandonnées.
supercat
@supercat Je suis d'accord, mais j'ai un problème avec la dernière phrase: si le transfert est vrai pour l'élément actuellement détenu, mais que l'élément nouvellement fourni est égal par référence à l'élément actuellement détenu, puis disposer de l'élément actuellement détenu est en train de disposer de l'article nouvellement fourni. Je pense que réapprovisionner le même article devrait être illégal.
Mike Nakis
Réapprovisionner le même article devrait généralement être illégal, sauf si l'objet est immuable, ne contient en fait aucune ressource et ne se soucie pas s'il est éliminé. Par exemple, si les Disposedemandes de brosses système étaient ignorées, une méthode "color.CreateBrush` pourrait à sa guise créer une nouvelle brosse (qui nécessiterait d'être supprimée) ou renvoyer une brosse système (qui ignorerait l'élimination). La manière la plus simple d'empêcher réutiliser un objet s'il se soucie de l'élimination, mais autoriser la réutilisation dans le cas contraire, c'est de l'éliminer
supercat
3

Je pense que vous avez raison, pour être honnête. Je pense que Microsoft a foiré la classe StreamWriter, en particulier, pour les raisons que vous décrivez.

Cependant, j'ai vu depuis beaucoup de code où les gens n'essaient même pas de disposer de leur propre Stream parce que le framework le fera pour eux, donc le réparer maintenant fera plus de mal que de bien.

pdr
la source
2

J'irais avec cohérence. Comme vous l'avez mentionné à la plupart des gens, il est logique que si votre objet crée un objet enfant, il le possédera et le détruira. Si on lui donne une référence de l'extérieur, à un moment donné "l'extérieur" détient également le même objet et il ne devrait pas lui appartenir. Si vous souhaitez transférer la propriété de l'extérieur vers votre objet, utilisez les méthodes Attacher / Détacher ou un constructeur qui accepte un booléen qui indique clairement que la propriété est transférée.

Après avoir tapé tout cela pour une réponse complète, je pense que la plupart des modèles de conception génériques ne tomberaient pas nécessairement dans la même catégorie que les classes StreamWriter / StreamReader. J'ai toujours pensé que la raison pour laquelle MS avait choisi que StreamWriter / Reader reprenne automatiquement la propriété est parce que dans ce cas spécifique, avoir une propriété partagée n'a pas beaucoup de sens. Vous ne pouvez pas avoir deux objets différents simultanément en lecture / écriture dans le même flux. Je suis sûr que vous pourriez écrire une sorte de part de temps déterministe, mais ce serait un enfer d'anti-modèle. C'est probablement pourquoi ils ont dit, car cela n'a aucun sens de partager un flux, prenons toujours le relais. Mais je ne considérerais pas que ce comportement est devenu une convention générique à tous les niveaux pour toutes les classes.

Vous pouvez considérer que Streams est un modèle cohérent où l'objet qui est transmis n'est pas partageable du point de vue de la conception et donc sans aucun indicateur supplémentaire, le lecteur / écrivain prend simplement le relais.

DXM
la source
J'ai clarifié ma question - avec cohérence, je voulais dire le comportement de toujours s'approprier.
ChrisWue
2

Faites attention. Ce que vous pensez de la «cohérence» pourrait simplement être un ensemble aléatoire d'habitudes.

"Coordination des interfaces utilisateur pour la cohérence", SIGCHI Bulletin 20, (1989), 63-65. Désolé, pas de lien, c'est un vieil article. "... un atelier de deux jours de 15 experts n'a pas été en mesure de produire une définition de la cohérence." Oui, il s'agit de "l'interface", mais je pense que si vous pensez à la "cohérence" pendant un certain temps, vous constaterez que vous ne pouvez pas non plus la définir.

Bruce Ediger
la source
vous avez raison, si les experts viennent de différents cercles, mais lors du développement du code, j'ai trouvé facile de suivre un schéma existant (ou une habitude si vous voulez) que mon équipe connaît déjà. Par exemple, l'autre jour, un de nos gars s'est enquis de certaines opérations asynchrones que nous avions et quand je lui ai dit qu'elles se comportaient de la même manière que Win32 Overlapped I / O, en 30 secondes, il a compris toute l'interface ... dans ce cas, les deux moi et lui venions du même arrière-plan du serveur Win32. Ftw de cohérence! :)
DXM
@Bruce Je comprends votre point de vue - je pensais que ce que je voulais dire par cohérence était clair, mais apparemment ce n'était pas le cas.
ChrisWue