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épendances 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 de l'implémentation "fuient" par 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 d'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 ?
On peut, bien sûr, invoquer la loi des abstractions qui fuient , mais ce n'est pas particulièrement intéressant car cela suppose que toutes les abstractions sont fuites. 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 qui fuit . Par conséquent, je vais d'abord essayer de définir comment je vois chacun de ces termes:
Ma définition préférée des abstractions est dérivée de l'application de Robert C. Martin [~ # ~] [~ # ~] :
"Une abstraction est l'amplification de l'essentiel et l'élimination de l'inutile."
Ainsi, les interfaces ne sont pas, en elles-mêmes, des abstractions . Ce ne sont des abstractions que si elles font remonter à la surface ce qui compte et cache le reste.
Le livre Principes, modèles et pratiques d'injection de dépendance définit le terme 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:
"les clients [...] possèdent les interfaces abstraites"
Cela signifie que les clients (code appelant) définissent les abstractions dont ils ont besoin, puis vous allez implémenter cette abstraction.
Une abstraction qui fuit , à mon avis, est une abstraction qui viole le DIP en incluant en quelque sorte des fonctionnalités que le client ne fait pas besoin .
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:
public class MaîtreD : IMaîtreD
{
public MaîtreD(int capacity, IReservationsRepository repository)
{
Capacity = capacity;
Repository = repository;
}
public int Capacity { get; }
public IReservationsRepository Repository { get; }
public int? TryAccept(Reservation reservation)
{
var reservations = Repository.ReadReservations(reservation.Date);
int reservedSeats = reservations.Sum(r => r.Quantity);
if (Capacity < reservedSeats + reservation.Quantity)
return null;
reservation.IsAccepted = true;
return Repository.Create(reservation);
}
}
Ici, la dépendance IReservationsRepository
est déterminée exclusivement par le client, la MaîtreD
classe:
public interface IReservationsRepository
{
Reservation[] ReadReservations(DateTimeOffset date);
int Create(Reservation reservation);
}
Cette interface est entièrement synchrone puisque le MaîtreD
la classe n'a pas besoin d'être asynchrone.
Vous pouvez facilement changer l'interface pour qu'elle soit asynchrone:
public interface IReservationsRepository
{
Task<Reservation[]> ReadReservations(DateTimeOffset date);
Task<int> Create(Reservation reservation);
}
Le MaîtreD
la classe, cependant, n'a pas besoin de ces méthodes pour être asynchrones, donc 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. La méthode TryAccept
doit désormais également devenir asynchrone:
public async Task<int?> TryAccept(Reservation reservation)
{
var reservations =
await Repository.ReadReservations(reservation.Date);
int reservedSeats = reservations.Sum(r => r.Quantity);
if (Capacity < reservedSeats + reservation.Quantity)
return null;
reservation.IsAccepted = true;
return await Repository.Create(reservation);
}
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.
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 de Async injection .
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 restent dans ma file d'attente d'articles, en attente de publication, alors restez à l'écoute.
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 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 devriez pas avoir à) vous soucier de la façon dont il est mis en œuvre.
L'attribut async
d'une méthode est une balise qui indique qu'un soin et une manipulation particuliers sont requis. En tant que tel, il a besoin de 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, au lieu de cela, votre bibliothèque gérait correctement toute l'activité asynchrone en elle-même, alors vous pouviez vous permettre de ne pas laisser async
"fuite" hors de 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.
Considérez les exemples suivants:
Il s'agit d'une méthode qui définit le nom avant son retour:
public void SetName(string name)
{
_dataLayer.SetName(name);
}
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 retournée n'est pas terminée (IsCompleted
= true):
public Task SetName(string name)
{
return _dataLayer.SetNameAsync(name);
}
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 retournée n'est pas terminée (IsCompleted
= true):
public async Task SetName(string name)
{
await _dataLayer.SetNameAsync(name);
}
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 mot clé async
; 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 de async
forme 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.
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" par l'abstraction elle-même.
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 il va y avoir des fuites dans certains sous-ensemble de cas (c'est-à-dire que toutes les abstractions sont fuyantes, la seule question est de savoir dans quelle mesure - ce qui signifie , auquel cas l'abstraction nous est-elle utile, 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 fuite abstraction est une abstraction 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, ils "fuient").
Ainsi, une interface exposant un détail d'une implémentation n'est pas étanche en soi (ou plutôt, une interface, considérée isolément, n'est pas en soi une abstraction qui fuit); à la place, la fuite dépend du code qui implémente l'interface (est-il capable de prendre en charge 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.)).
Il s'agit d'une abstraction qui fuit si et seulement si vous faites pas l'intention de toutes les classes d'implémentation de créer 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.
Voici un point de vue opposé.
Nous ne sommes pas passés du retour de Foo
au retour de Task<Foo>
parce que nous avons commencé à vouloir le Task
au lieu de simplement le Foo
. Certes, nous interagissons parfois avec le Task
mais dans la plupart des codes du monde réel nous l'ignorons et utilisons simplement le Foo
.
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 renvoie un Task<Foo>
vous indique que l'implémentation est peut-être asynchrone, qu'elle le soit ou non, même si cela peut vous déranger ou non. Si une abstraction nous en dit plus que ce que nous avons besoin de savoir sur sa mise en œuvre, elle fuit.
Si notre implémentation n'est pas asynchrone, nous la changeons 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 de celui-ci pourrait être quelque chose qui est en fait 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 selon laquelle "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 mauvaise abstraction ou elle peut refléter sa mise en œuvre 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.