web-dev-qa-db-fra.com

Les directives asynchrones / en attente d'utilisation en C # ne contredisent-elles pas les concepts de bonne architecture et de superposition d'abstraction?

Cette question concerne le langage C #, mais je m'attends à ce qu'il couvre d'autres langages tels que Java ou TypeScript.

Microsoft recommande meilleures pratiques sur l'utilisation d'appels asynchrones dans .NET. Parmi ces recommandations, choisissons-en deux:

  • changer la signature des méthodes asynchrones pour qu'elles renvoient Task ou Task <> (en TypeScript, ce serait une promesse <>)
  • changer les noms des méthodes asynchrones pour se terminer par xxxAsync ()

Désormais, lorsque vous remplacez un composant synchrone de bas niveau par un composant asynchrone, cela affecte la pile complète de l'application. Étant donné que async/wait n'a un impact positif que s'il est utilisé "tout le long", cela signifie que les noms de signature et de méthode de chaque couche de l'application doivent être modifiés.

Une bonne architecture implique souvent de placer des abstractions entre chaque couche, de sorte que le remplacement de composants de bas niveau par d'autres n'est pas visible par les composants de niveau supérieur. En C #, les abstractions prennent la forme d'interfaces. Si nous introduisons un nouveau composant asynchrone de bas niveau, chaque interface de la pile d'appels doit être soit modifiée, soit remplacée par une nouvelle interface. La façon dont un problème est résolu (async ou sync) dans une classe d'implémentation n'est plus cachée (abstraite) aux appelants. Les appelants doivent savoir s'il s'agit d'une synchronisation ou d'une asynchronisation.

N'est-ce pas asynchrone/attendre les meilleures pratiques en contradiction avec les principes de "bonne architecture"?

Cela signifie-t-il que chaque interface (disons IEnumerable, IDataAccessLayer) a besoin de son homologue asynchrone (IAsyncEnumerable, IAsyncDataAccessLayer) de sorte qu'elles puissent être remplacées dans la pile lors du passage aux dépendances asynchrones?

Si nous poussons le problème un peu plus loin, ne serait-il pas plus simple de supposer que chaque méthode est asynchrone (pour renvoyer une tâche <> ou une promesse <>), et que les méthodes synchronisent les appels asynchrones quand ils ne sont pas réellement async? Est-ce quelque chose à attendre des futurs langages de programmation?

106
corentinaltepe

De quelle couleur est votre fonction?

Vous pouvez être intéressé par Bob Nystrom Quelle couleur est votre fonction1.

Dans cet article, il décrit un langage fictif où:

  • Chaque fonction a une couleur: bleu ou rouge.
  • Une fonction rouge peut appeler des fonctions bleues ou rouges, aucun problème.
  • Une fonction bleue ne peut appeler que des fonctions bleues.

Bien que fictif, cela se produit assez régulièrement dans les langages de programmation:

  • En C++, une méthode "const" ne peut appeler d'autres méthodes "const" que sur this.
  • Dans Haskell, une fonction non IO ne peut appeler que des fonctions non IO.
  • En C #, une fonction de synchronisation ne peut appeler que des fonctions de synchronisation2.

Comme vous l'avez compris, à cause de ces règles, les fonctions rouges ont tendance à se propager autour de la base de code. Vous en insérez un, et petit à petit il colonise toute la base de code.

1  Bob Nystrom, outre les blogs, fait également partie de l'équipe Dart et a écrit cette petite série Crafting Interpreters; fortement recommandé pour tout afficionado de langage de programmation/compilateur.

2  Pas tout à fait vrai, car vous pouvez appeler une fonction asynchrone et bloquer jusqu'à ce qu'elle revienne, mais ...

Limitation de langue

Il s'agit essentiellement d'une limitation de langue/d'exécution.

Le langage avec le filetage M: N, par exemple, comme Erlang et Go, n'a pas de fonctions async: chaque fonction est potentiellement asynchrone et sa "fibre" sera simplement suspendue, permutée et réintégrée lorsque il est de nouveau prêt.

C # a opté pour un modèle de thread 1: 1 et a donc décidé de faire apparaître la synchronicité dans le langage pour éviter de bloquer accidentellement les threads.

En présence de limitations linguistiques, les directives de codage doivent s'adapter.

110
Matthieu M.

Vous avez raison, il y a une contradiction ici, mais ce ne sont pas les "meilleures pratiques" qui sont mauvaises. C'est parce que la fonction asynchrone fait essentiellement autre chose qu'une fonction synchrone. Au lieu d'attendre le résultat de ses dépendances (généralement des IO), il crée une tâche à gérer par la boucle d'événement principale. Ce n'est pas une différence qui peut être bien cachée sous abstraction.

82
max630

Une méthode asynchrone se comporte différemment d'une méthode synchrone, comme vous le savez sûrement. Au moment de l'exécution, convertir un appel asynchrone en un appel synchrone est trivial, mais l'inverse ne peut pas être dit. Donc, par conséquent, la logique devient alors, pourquoi ne faisons-nous pas des méthodes asynchrones de toutes les méthodes qui peuvent l'exiger et laissons l'appelant se "convertir" si nécessaire en une méthode synchrone?

Dans un sens, c'est comme avoir une méthode qui lève des exceptions et une autre qui est "sûre" et ne lève pas même en cas d'erreur. À quel moment le codeur est-il excessif pour fournir ces méthodes qui, autrement, peuvent être converties l'une en l'autre?

En cela, il y a deux écoles de pensée: l'une consiste à créer plusieurs méthodes, chacune appelant une autre méthode éventuellement privée permettant la possibilité de fournir des paramètres facultatifs ou des modifications mineures au comportement, comme être asynchrone. L'autre consiste à réduire au minimum les méthodes d'interface pour ne laisser que l'essentiel, laissant à l'appelant le soin d'effectuer lui-même les modifications nécessaires.

Si vous êtes de la première école, il y a une certaine logique à dédier une classe aux appels synchrones et asynchrones afin d'éviter de doubler chaque appel. Microsoft a tendance à favoriser cette école de pensée, et par convention, pour rester cohérent avec le style privilégié par Microsoft, vous aussi devriez avoir une version Async, de la même manière que les interfaces commencent presque toujours par un "je". Permettez-moi de souligner que ce n'est pas faux, en soi, car il vaut mieux conserver un style cohérent dans un projet plutôt que de le faire "de la bonne façon" et changer radicalement de style pour le développement que vous ajouter à un projet.

Cela dit, j'ai tendance à privilégier la deuxième école, qui est de minimiser les méthodes d'interface. Si je pense qu'une méthode peut être appelée de manière asynchrone, la méthode pour moi est asynchrone. L'appelant peut décider d'attendre ou non la fin de cette tâche avant de poursuivre. Si cette interface est une interface vers une bibliothèque, il est plus raisonnable de le faire de cette façon pour minimiser le nombre de méthodes que vous devez déprécier ou ajuster. Si l'interface est à usage interne dans mon projet, j'ajouterai une méthode pour chaque appel nécessaire tout au long de mon projet pour les paramètres fournis et pas de méthodes "supplémentaires", et même alors, seulement si le comportement de la méthode n'est pas déjà couvert par une méthode existante.

Cependant, comme beaucoup de choses dans ce domaine, c'est largement subjectif. Les deux approches ont leurs avantages et leurs inconvénients. Microsoft a également commencé la convention consistant à ajouter des lettres indiquant le type au début du nom de la variable et "m_" pour indiquer qu'il s'agit d'un membre, ce qui conduit à des noms de variable comme m_pUser. Mon point étant que même Microsoft n'est pas infaillible et peut aussi faire des erreurs.

Cela dit, si votre projet suit cette convention Async, je vous conseille de la respecter et de continuer le style. Et seulement une fois que vous avez reçu votre propre projet, vous pouvez l'écrire de la meilleure façon qui vous convient.

6
Neil

Imaginons qu'il existe un moyen de vous permettre d'appeler des fonctions de manière asynchrone sans modifier leur signature.

Ce serait vraiment cool et personne ne vous recommanderait de changer leurs noms.

Mais, les fonctions asynchrones réelles, pas seulement celles qui attendent une autre fonction asynchrone, mais le niveau le plus bas ont une structure spécifique à leur nature asynchrone. par exemple

public class HTTPClient
{
    public HTTPResponse GET()
    {
        //send data
        while(!timedOut)
        {
            //check for response
            if(response) { 
                this.GotResponse(response); 
            }
            this.YouCanWait();
        }
    }

    //tell calling code that they should watch for this event
    public EventHander GotResponse
    //indicate to calling code that they can go and do something else for a bit
    public EventHander YouCanWait;
}

Ce sont ces deux informations dont le code appelant a besoin pour exécuter le code de manière asynchrone que des choses comme Task et async encapsulent.

Il existe plusieurs façons d'effectuer des fonctions asynchrones, async Task n'est qu'un modèle intégré au compilateur via les types de retour afin que vous n'ayez pas à lier manuellement les événements

2
Ewan

Je vais aborder le point principal d'une manière moins C # ness et plus générique:

N'est-ce pas asynchrone/attendre les meilleures pratiques en contradiction avec les principes de "bonne architecture"?

Je dirais que cela dépend simplement du choix que vous faites dans la conception de votre API et de ce que vous laissez à l'utilisateur.

Si vous souhaitez qu'une fonction de votre API soit uniquement asynchrone, il y a peu d'intérêt à suivre la convention de dénomination. Renvoyez toujours Task <>/Promise <>/Future <>/... comme type de retour, il est auto-documenté. S'il veut une réponse synchronisée, il pourra toujours le faire en attendant, mais s'il le fait toujours, cela fait un peu de passe-partout.

Cependant, si vous effectuez uniquement la synchronisation de votre API, cela signifie que si un utilisateur souhaite qu'il soit asynchrone, il devra gérer lui-même la partie asynchrone.

Cela peut faire beaucoup de travail supplémentaire, mais cela peut également donner plus de contrôle à l'utilisateur sur le nombre d'appels simultanés qu'il autorise, le délai d'expiration, les retrys, etc.

Dans un grand système avec une énorme API, implémenter la plupart d'entre elles pour être synchronisées par défaut pourrait être plus facile et plus efficace que de gérer indépendamment chaque partie de votre API, surtout si elles partagent des ressources (système de fichiers, CPU, base de données, ...).

En fait, pour les parties les plus complexes, vous pourriez parfaitement avoir deux implémentations de la même partie de votre API, une synchrone faisant les trucs pratiques, une asynchrone s'appuyant sur la synchrone qui gère les trucs et ne gérant que la concurrence, les chargements, les délais d'attente et les tentatives .

Peut-être que quelqu'un d'autre peut partager son expérience avec cela parce que je manque d'expérience avec de tels systèmes.

0
Walfrat