Je lis le livre Principes, pratiques et modèles d'injection de dépendance et j'ai lu sur le concept d'abstraction qui fuit qui est bien décrit dans le livre.
Ces jours-ci, je refactorise une base de code C # en utilisant l'injection de dépendance afin que les appels asynchrones soient utilisés au lieu de bloquer ceux-ci. Ce faisant, je considère certaines interfaces qui représentent des abstractions dans ma base de code et qui doivent être repensées afin que les appels asynchrones puissent être utilisés.
Par exemple, considérons l'interface suivante représentant un référentiel pour les utilisateurs d'applications:
public interface IUserRepository
{
Task<IEnumerable<User>> GetAllAsync();
}
Selon la définition du livre, une abstraction qui fuit est une abstraction conçue avec une implémentation spécifique à l'esprit, de sorte que certains détails d'implémentation "fuient" à travers l'abstraction elle-même.
Ma question est la suivante: pouvons-nous considérer une interface conçue avec async à l'esprit, comme IUserRepository, comme exemple d'une abstraction qui fuit?
Bien sûr, toutes les implémentations possibles n'ont rien à voir avec l'asynchronie: seules les implémentations hors processus (comme une implémentation SQL) le font, mais un référentiel en mémoire ne nécessite pas l'asynchronie (l'implémentation d'une version en mémoire de l'interface est probablement plus difficile si l'interface expose des méthodes asynchrones, par exemple, vous devrez probablement renvoyer quelque chose comme Task.CompletedTask ou Task.FromResult (utilisateurs) dans les implémentations de méthode).
Qu'est ce que tu penses de ça ?
la source
Task
. Les directives pour suffixer les méthodes asynchrones avec le mot async consistaient à distinguer les appels API par ailleurs identiques (répartition C # ne peut pas être basée sur le type de retour). Dans notre entreprise, nous avons tout abandonné ensemble.Réponses:
On peut, bien sûr, invoquer la loi des abstractions qui fuient , mais ce n'est pas particulièrement intéressant car cela postule que toutes les abstractions sont fuyantes. On peut argumenter pour et contre cette conjecture, mais cela n'aide pas si nous ne partageons pas une compréhension de ce que nous entendons par abstraction et de ce que nous entendons par fuite . Par conséquent, je vais d'abord essayer de définir comment je vois chacun de ces termes:
Abstractions
Ma définition préférée des abstractions est dérivée de l' APPP de Robert C. Martin :
Ainsi, les interfaces ne sont pas, en soi, des abstractions . Ce ne sont des abstractions que si elles mettent à la surface ce qui compte et cache le reste.
Qui fuit
Le livre Dependency Injection Principles, Patterns and Practices définit le terme d' abstraction qui fuit dans le contexte de l'injection de dépendance (DI). Le polymorphisme et les principes SOLID jouent un grand rôle dans ce contexte.
Du principe d'inversion de dépendance (DIP), il s'ensuit, citant encore APPP, que:
Cela signifie que les clients (code appelant) définissent les abstractions dont ils ont besoin, puis vous implémentez cette abstraction.
Une abstraction qui fuit , à mon avis, est une abstraction qui viole le DIP en incluant en quelque sorte des fonctionnalités dont le client n'a pas besoin .
Dépendances synchrones
Un client qui implémente un élément de logique métier utilise généralement DI pour se dissocier de certains détails d'implémentation, tels que, généralement, les bases de données.
Considérons un objet de domaine qui gère une demande de réservation de restaurant:
Ici, la
IReservationsRepository
dépendance est déterminée exclusivement par le client, laMaîtreD
classe:Cette interface est entièrement synchrone car la
MaîtreD
classe n'a pas besoin qu'elle soit asynchrone.Dépendances asynchrones
Vous pouvez facilement changer l'interface pour qu'elle soit asynchrone:
La
MaîtreD
classe, cependant, n'a pas besoin de ces méthodes pour être asynchrones, maintenant le DIP est violé. Je considère cela comme une abstraction qui fuit, car un détail d'implémentation oblige le client à changer. LaTryAccept
méthode doit désormais également devenir asynchrone:Il n'y a aucune raison inhérente pour que la logique du domaine soit asynchrone, mais pour prendre en charge l'asynchronie de l'implémentation, cela est maintenant requis.
De meilleures options
Au NDC Sydney 2018, j'ai donné une conférence sur ce sujet . Dans ce document, je présente également une alternative qui ne fuit pas. Je donnerai également cette conférence lors de plusieurs conférences en 2019, mais maintenant renommée avec le nouveau titre d' injection Async .
Je prévois également de publier une série de billets de blog pour accompagner la conférence. Ces articles sont déjà écrits et se trouvent dans ma file d'attente d'articles, en attente de publication, alors restez à l'écoute.
la source
Ce n'est pas du tout une abstraction qui fuit.
Être asynchrone est un changement fondamental dans la définition d'une fonction - cela signifie que la tâche n'est pas terminée lorsque l'appel revient, mais cela signifie également que le flux de votre programme se poursuivra presque immédiatement, pas avec un long délai. Une fonction asynchrone et une fonction synchrone effectuant la même tâche sont essentiellement des fonctions différentes. Être asynchrone n'est pas un détail d'implémentation. Cela fait partie de la définition d'une fonction.
Si la fonction révélait comment la fonction était rendue asynchrone, ce serait une fuite. Vous (ne devez / ne devez pas) vous soucier de la façon dont il est mis en œuvre.
la source
L'
async
attribut d'une méthode est une balise qui indique qu'un soin et une manipulation particuliers sont nécessaires. En tant que tel, il doit fuir dans le monde. Les opérations asynchrones sont extrêmement difficiles à composer correctement, il est donc important d'avertir l'utilisateur de l'API.Si, à la place, votre bibliothèque gérait correctement toutes les activités asynchrones en elle-même, alors vous pouviez vous permettre de ne pas laisser
async
"fuir" l'API.Le logiciel comporte quatre dimensions de difficulté: les données, le contrôle, l'espace et le temps. Les opérations asynchrones couvrent les quatre dimensions et nécessitent donc le plus de soins.
la source
Pas assez. Une abstraction est une chose conceptuelle qui ignore certains éléments d'une chose ou d'un problème concret plus compliqué (pour rendre la chose / le problème plus simple, traitable ou en raison d'un autre avantage). En tant que tel, il est nécessairement différent de la chose / du problème réel, et donc cela va être fuyant dans certains sous-ensembles de cas (c'est-à-dire que toutes les abstractions sont fuyantes, la seule question est de savoir dans quelle mesure - c'est-à-dire, dans quels cas est l'abstraction utile pour nous, quel est son domaine d’applicabilité).
Cela dit, en ce qui concerne les abstractions logicielles, parfois (ou peut-être assez souvent?) Les détails que nous avons choisi d'ignorer ne peuvent pas être ignorés car ils affectent certains aspects du logiciel qui sont importants pour nous (performances, maintenabilité, ...) . Donc, une abstraction qui fuit est une abstraction qui a été conçue pour ignorer certains détails (en supposant qu'il était possible et utile de le faire), mais il s'est avéré que certains de ces détails sont importants dans la pratique (ils ne peuvent pas être ignorés, donc ils "fuite").
Ainsi, une interface exposant un détail d'une implémentation n'est pas une fuite en soi (ou plutôt, une interface, considérée isolément, n'est pas en soi une abstraction qui fuit); au lieu de cela, la fuite dépend du code qui implémente l'interface (est-il capable de prendre en charge réellement l'abstraction représentée par l'interface), ainsi que des hypothèses émises par le code client (qui équivalent à une abstraction conceptuelle qui complète celle exprimée par l'interface, mais ne peut pas être elle-même exprimée en code (par exemple, les fonctionnalités du langage ne sont pas assez expressives, nous pouvons donc la décrire dans la documentation, etc.)).
la source
Considérez les exemples suivants:
Il s'agit d'une méthode qui définit le nom avant son retour:
Il s'agit d'une méthode qui définit le nom. L'appelant ne peut pas supposer que le nom est défini tant que la tâche renvoyée n'est pas terminée (
IsCompleted
= true):Il s'agit d'une méthode qui définit le nom. L'appelant ne peut pas supposer que le nom est défini tant que la tâche renvoyée n'est pas terminée (
IsCompleted
= true):Q: Lequel n'appartient pas aux deux autres?
R: La méthode asynchrone n'est pas celle qui est autonome. Celui qui est seul est la méthode qui renvoie le vide.
Pour moi, la "fuite" ici n'est pas le
async
mot - clé; c'est le fait que la méthode retourne une tâche. Et ce n'est pas une fuite; cela fait partie du prototype et fait partie de l'abstraction. Une méthode asynchrone qui renvoie une tâche fait exactement la même promesse faite par une méthode synchrone qui renvoie une tâche.Donc non, je ne pense pas que l'introduction des
async
formes soit une abstraction qui fuit en soi. Mais vous devrez peut-être changer le prototype pour renvoyer une tâche, qui "fuit" en changeant l'interface (l'abstraction). Et comme cela fait partie de l'abstraction, ce n'est pas une fuite, par définition.la source
Il s'agit d'une abstraction qui fuit si et seulement si vous n'avez pas l' intention que toutes les classes d'implémentation créent un appel asynchrone. Vous pouvez créer plusieurs implémentations, par exemple, une pour chaque type de base de données que vous prenez en charge, et ce serait tout à fait correct en supposant que vous n'ayez jamais besoin de connaître l'implémentation exacte utilisée dans votre programme.
Et bien que vous ne puissiez pas appliquer strictement une implémentation asynchrone, le nom implique qu'elle devrait l'être. Si les circonstances changent et qu'il peut s'agir d'un appel synchrone pour quelque raison que ce soit, vous devrez peut-être envisager un changement de nom, donc mon conseil serait de le faire uniquement si vous ne pensez pas que cela sera très probable dans le avenir.
la source
Voici un point de vue opposé.
Nous ne sommes pas allés de retour
Foo
en retourTask<Foo>
parce que nous avons commencé à vouloir leTask
au lieu de juste leFoo
. Certes, nous interagissons parfois avec leTask
mais dans la plupart des codes du monde réel, nous l'ignorons et utilisons simplement leFoo
.De plus, nous définissons souvent des interfaces pour prendre en charge le comportement asynchrone même lorsque l'implémentation peut être asynchrone ou non.
En effet, une interface qui retourne un
Task<Foo>
vous indique que l'implémentation est peut-être asynchrone, qu'elle le soit vraiment ou non, même si vous vous en souciez ou non. Si une abstraction nous en dit plus que ce dont nous avons besoin de savoir sur sa mise en œuvre, elle fuit.Si notre implémentation n'est pas asynchrone, nous la modifions pour être asynchrone, puis nous devons changer l'abstraction et tout ce qui l'utilise, c'est une abstraction très fuyante.
Ce n'est pas un jugement. Comme d'autres l'ont souligné, toutes les abstractions fuient. Celui-ci a un impact plus important car il nécessite un effet d'entraînement asynchrone / attend dans tout notre code simplement parce que quelque part à la fin, il pourrait y avoir quelque chose qui est réellement asynchrone.
Cela ressemble-t-il à une plainte? Ce n'est pas mon intention, mais je pense que c'est une observation exacte.
Un point connexe est l'affirmation qu '"une interface n'est pas une abstraction". Ce que Mark Seeman a succinctement déclaré a été un peu abusé.
La définition de «abstraction» n'est pas «interface», même dans .NET. Les abstractions peuvent prendre de nombreuses autres formes. Une interface peut être une abstraction médiocre ou elle peut refléter son implémentation si étroitement que, dans un sens, ce n'est guère une abstraction.
Mais nous utilisons absolument des interfaces pour créer des abstractions. Donc, jeter "les interfaces ne sont pas des abstractions" car une question mentionne les interfaces et les abstractions n'est pas éclairant.
la source
Est-ce
GetAllAsync()
réellement asynchrone? Je veux dire que "async" est dans le nom, mais cela peut être supprimé. Alors je demande à nouveau ... Est-il impossible d'implémenter une fonction qui renvoie unTask<IEnumerable<User>>
qui est résolu de manière synchrone?Je ne connais pas les spécificités du
Task
type de .Net , mais s'il est impossible d'implémenter la fonction de manière synchrone, alors assurez-vous que c'est une abstraction qui fuit (de cette façon) mais sinon pas. Je ne sais que si elle étaitIObservable
plutôt que d' une tâche, il pourrait être mis en œuvre de manière synchrone ou asynchrone donc rien en dehors de la fonction sait et il est donc ne fuit pas ce fait particulier.la source
Task<T>
signifie asynchrone. Vous obtenez l'objet de tâche immédiatement, mais devrez peut-être attendre la séquence d'utilisateurs