Un'interfaccia che espone le funzioni asincrone è un'astrazione che perde?


13

Sto leggendo il libro Principi, pratiche e schemi dell'iniezione di dipendenza e ho letto il concetto di astrazione che perde, che è ben descritto nel libro.

In questi giorni sto refactoring di una base di codice C # usando l'iniezione di dipendenza in modo che vengano utilizzate chiamate asincrone invece di bloccare quelle. In questo modo sto prendendo in considerazione alcune interfacce che rappresentano le astrazioni nella mia base di codice e che devono essere riprogettate in modo da poter utilizzare le chiamate asincrone.

Ad esempio, considerare la seguente interfaccia che rappresenta un repository per gli utenti dell'applicazione:

public interface IUserRepository 
{
  Task<IEnumerable<User>> GetAllAsync();
}

Secondo la definizione del libro, un'astrazione che perde è un'astrazione progettata pensando a un'implementazione specifica, in modo che alcuni dettagli dell'implementazione "trapelino" attraverso l'astrazione stessa.

La mia domanda è la seguente: possiamo considerare un'interfaccia progettata pensando all'asincrono, come IUserRepository, come esempio di Leaky Abstraction?

Naturalmente non tutte le possibili implementazioni hanno qualcosa a che fare con l'asincronia: solo le implementazioni fuori processo (come un'implementazione SQL) lo fanno, ma un repository in memoria non richiede l'asincronia (l'implementazione di una versione in memoria dell'interfaccia è probabilmente più difficile se l'interfaccia espone metodi asincroni, ad esempio probabilmente è necessario restituire qualcosa come Task.CompletedTask o Task.FromResult (utenti) nelle implementazioni del metodo).

Cosa ne pensi di questo ?


@Neil probabilmente ho capito il punto. Un'interfaccia che espone metodi che restituiscono Task o Task <T> non è un'astrazione che perde di per sé, è semplicemente un contratto con una firma che coinvolge attività. Un metodo che restituisce un'attività o attività <T> non implica avere un'implementazione asincrona (ad esempio se creo un'attività completata utilizzando Task.CompletedTask Non sto eseguendo un'implementazione asincrona). Viceversa, l'implementazione asincrona in C # richiede che il tipo restituito di un metodo asincrono debba essere di tipo Task o Task <T>. Detto in altro modo, l'unico aspetto "che perde" della mia interfaccia è il suffisso asincrono nei nomi
Enrico Massone,

@Neil in realtà esiste una linea guida per la denominazione che afferma che tutti i metodi asincroni dovrebbero avere un nome che termina con "Async". Ciò non implica che un metodo che restituisce un'attività o un'attività <T> debba essere denominato con il suffisso Async perché potrebbe essere implementato senza chiamate asincrone.
Enrico Massone,

6
Direi che l'asincrono di un metodo è indicato dal fatto che restituisce a Task. Le linee guida per il suffisso dei metodi asincroni con la parola asincrona consistevano nel distinguere tra chiamate API altrimenti identiche (non è possibile inviare C # in base al tipo restituito). Nella nostra azienda abbiamo lasciato perdere tutto insieme.
Richzilla,

Ci sono una serie di risposte e commenti che spiegano perché la natura asincrona del metodo fa parte dell'astrazione. Una domanda più interessante è come un linguaggio o un'API di programmazione può separare la funzionalità di un metodo da come viene eseguito, al punto in cui non abbiamo più bisogno di valori di restituzione dell'attività o di marcatori asincroni? I programmatori funzionali sembrano averlo capito meglio. Considera come i metodi asincroni sono definiti in F # e in altre lingue.
Frank Hileman,

2
:-) -> Il "programmatore funzionale" ha. Async non è più permeabile che sincrono, sembra solo così perché siamo abituati a scrivere il codice di sincronizzazione per impostazione predefinita. Se tutti noi abbiamo codificato asincrono per impostazione predefinita, una funzione sincrona potrebbe sembrare che perde.
StarTrekRedneck,

Risposte:


8

Naturalmente, si può invocare la legge delle astrazioni che perdono , ma ciò non è particolarmente interessante perché presuppone che tutte le astrazioni siano trapelate. Si può discutere a favore e contro quella congettura, ma non aiuta se non condividiamo la comprensione di cosa intendiamo per astrazione e cosa intendiamo per perdita . Pertanto, cercherò innanzitutto di delineare il modo in cui visualizzo ciascuno di questi termini:

astrazioni

La mia definizione preferita di astrazioni deriva dall'APPP di Robert C. Martin :

"Un'astrazione è l'amplificazione dell'essenziale e l'eliminazione dell'irrilevante."

Pertanto, le interfacce non sono, di per sé, astrazioni . Sono solo astrazioni se mettono in superficie ciò che conta e nasconde il resto.

Che perde

Il libro Principi, schemi e pratiche dell'iniezione di dipendenza definisce il termine astrazione che perde nel contesto di Iniezione di dipendenza (DI). Il polimorfismo e i principi SOLIDI svolgono un ruolo importante in questo contesto.

Dal principio di inversione di dipendenza (DIP) segue, citando ancora APPP, che:

"i client [...] possiedono le interfacce astratte"

Ciò significa che i client (codice chiamante) definiscono le astrazioni di cui hanno bisogno, quindi si va e si implementa tale astrazione.

Un'astrazione che perde , a mio avviso, è un'astrazione che viola il DIP includendo in qualche modo alcune funzionalità che il cliente non ha bisogno .

Dipendenze sincrone

Un client che implementa un pezzo di logica aziendale in genere utilizzerà DI per separarsi da alcuni dettagli di implementazione, come, comunemente, database.

Considera un oggetto di dominio che gestisce una richiesta per una prenotazione di un ristorante:

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);
    }
}

Qui, la IReservationsRepositorydipendenza è determinata esclusivamente dal client, la MaîtreDclasse:

public interface IReservationsRepository
{
    Reservation[] ReadReservations(DateTimeOffset date);
    int Create(Reservation reservation);
}

Questa interfaccia è completamente sincrona poiché la MaîtreDclasse non ha bisogno che sia asincrona.

Dipendenze asincrone

Puoi facilmente cambiare l'interfaccia in modo che sia asincrona:

public interface IReservationsRepository
{
    Task<Reservation[]> ReadReservations(DateTimeOffset date);
    Task<int> Create(Reservation reservation);
}

La MaîtreDclasse, tuttavia, non ha bisogno che questi metodi siano asincroni, quindi ora il DIP viene violato. Lo considero un'astrazione che perde, perché un dettaglio di implementazione costringe il cliente a cambiare. Il TryAcceptmetodo ora deve anche diventare asincrono:

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);
}

Non esiste una logica intrinseca per cui la logica del dominio sia asincrona, ma per supportare l'asincronia dell'implementazione, questo è ora necessario.

Migliori opzioni

A NDC Sydney 2018 ho tenuto un discorso su questo argomento . In esso, descrivo anche un'alternativa che non perde. Darò questo discorso anche in diverse conferenze nel 2019, ma ora rinominato con il nuovo titolo di Async injection .

Ho intenzione di pubblicare anche una serie di post sul blog per accompagnare il discorso. Questi articoli sono già scritti e seduti nella mia coda di articoli, in attesa di essere pubblicati, quindi rimanete sintonizzati.


Secondo me questa è una questione di intenti. Se la mia astrazione appare come se dovesse comportarsi in un modo ma qualche dettaglio o vincolo rompe l'astrazione come presentata, questa è un'astrazione che perde. Ma in questo caso, ti sto esplicitamente presentando che l'operazione è asincrona - non è quello che sto cercando di astrarre. Questo è distinto nella mia mente dal tuo esempio in cui sto (saggiamente o no) cercando di sottrarre il fatto che esiste un database SQL e continuo a esporre una stringa di connessione. Forse è una questione di semantica / prospettiva.
Formica P

Quindi possiamo dire che un'astrazione non è mai una perdita "in sé", invece è una perdita se alcuni dettagli di un'implementazione specifica fuoriescono dagli elementi esposti e costringono il consumatore a cambiarne l'implementazione, al fine di soddisfare la forma dell'astrazione .
Enrico Massone,

2
È interessante notare che il punto che hai evidenziato nella tua spiegazione è uno dei punti più fraintesi dell'intera storia dell'iniezione di dipendenza. A volte gli sviluppatori dimenticano il principio di inversione di dipendenza e provano prima a progettare l'astrazione e poi adattano il design del consumatore per far fronte all'astrazione stessa. Invece, il processo dovrebbe essere eseguito nell'ordine inverso.
Enrico Massone,

11

Non è affatto un'astrazione che perde.

Essere asincroni è una modifica fondamentale alla definizione di una funzione: significa che l'attività non è terminata quando la chiamata ritorna, ma significa anche che il flusso del programma continuerà quasi immediatamente, non con un lungo ritardo. Una funzione asincrona e una sincrona che svolgono la stessa attività sono essenzialmente funzioni diverse. Essere asincroni non è un dettaglio di implementazione. Fa parte della definizione di una funzione.

Se la funzione esponesse il modo in cui la funzione è stata resa asincrona, ciò avrebbe perdite. Non devi / non dovresti preoccuparti di come viene implementato.


5

L' asyncattributo di un metodo è un tag che indica che è richiesta particolare cura e gestione. Come tale, deve uscire nel mondo. Le operazioni asincrone sono estremamente difficili da comporre correttamente, quindi è importante dare un avvertimento all'utente API.

Se, invece, la tua libreria gestisse correttamente tutte le attività asincrone al suo interno, allora potresti permetterti di non lasciare async"trapelare" dall'API.

Ci sono quattro dimensioni di difficoltà nel software: dati, controllo, spazio e tempo. Le operazioni asincrone si estendono su tutte e quattro le dimensioni, quindi richiedono la massima cura.


Sono d'accordo con il tuo sentimento, ma "perdita" implica qualcosa di brutto, che è l'intento del termine "astrazione che perde" - qualcosa di indesiderabile nell'astrazione. Nel caso di asincrono vs sincronizzazione, nulla perde.
StarTrekRedneck,

2

un'astrazione che perde è un'astrazione progettata pensando a un'implementazione specifica, in modo che alcuni dettagli dell'implementazione "trapelino" attraverso l'astrazione stessa.

Non proprio. Un'astrazione è una cosa concettuale che ignora alcuni elementi di una cosa concreta o un problema più complicato (per rendere la cosa / il problema più semplice, trattabile o dovuto ad altri benefici). Come tale, è necessariamente diverso dalla cosa / problema reale, e quindi sarà trapelato in alcuni sottogruppi di casi (cioè, tutte le astrazioni sono trapelate, l'unica domanda è fino a che punto - significato, in quali casi è l'astrazione utile per noi, qual è il suo dominio di applicabilità).

Detto questo, quando si tratta di astrazioni del software, a volte (o forse abbastanza spesso?) I dettagli che abbiamo scelto di ignorare non possono in realtà essere ignorati perché influenzano alcuni aspetti del software che sono importanti per noi (prestazioni, manutenibilità, ...) . Quindi un'astrazione che perde è un'astrazione progettata per ignorare alcuni dettagli (presupponendo che fosse possibile e utile farlo), ma poi si è scoperto che alcuni di questi dettagli sono significativi nella pratica (non possono essere ignorati, quindi "trapelare").

Quindi, un'interfaccia che espone un dettaglio di un'implementazione non è di per sé leale (o piuttosto, un'interfaccia, vista in isolamento, non è di per sé un'astrazione che perde); invece, la perdita dipende dal codice che implementa l'interfaccia (è in grado di supportare effettivamente l'astrazione rappresentata dall'interfaccia), e anche dalle ipotesi fatte dal codice client (che equivalgono a un'astrazione concettuale che completa quella espressa da l'interfaccia, ma non può di per sé essere espressa in codice (ad es. le caratteristiche del linguaggio non sono abbastanza espressive, quindi possiamo descriverlo nei documenti, ecc.)).


2

Considera i seguenti esempi:

Questo è un metodo che imposta il nome prima che ritorni:

public void SetName(string name)
{
    _dataLayer.SetName(name);
}

Questo è un metodo che imposta il nome. Il chiamante non può presumere che il nome sia impostato fino al completamento dell'attività restituita ( IsCompleted= true):

public Task SetName(string name)
{
    return _dataLayer.SetNameAsync(name);
}

Questo è un metodo che imposta il nome. Il chiamante non può presumere che il nome sia impostato fino al completamento dell'attività restituita ( IsCompleted= true):

public async Task SetName(string name)
{
    await _dataLayer.SetNameAsync(name);
}

D: Quale non appartiene agli altri due?

A: Il metodo asincrono non è quello che sta da solo. Quello che è solo è il metodo che restituisce il vuoto.

Per me, la "perdita" qui non è la asyncparola chiave; è il fatto che il metodo restituisce un'attività. E questa non è una perdita; fa parte del prototipo e parte dell'astrazione. Un metodo asincrono che restituisce un'attività fa esattamente la stessa promessa fatta da un metodo sincrono che restituisce un'attività.

Quindi no, non penso che l'introduzione di asyncforme sia un'astrazione che perde in sé e per sé. Ma potresti dover cambiare il prototipo per restituire un'attività, che "perde" cambiando l'interfaccia (l'astrazione). E poiché fa parte dell'astrazione, non è una perdita, per definizione.


0

Questa è un'astrazione che perde se e solo se non si intende che tutte le classi di implementazione creino una chiamata asincrona. È possibile creare più implementazioni, ad esempio, una per ogni tipo di database supportato, e questo sarebbe perfettamente a posto supponendo che non avessi mai bisogno di conoscere l'esatta implementazione utilizzata nel tuo programma.

E mentre non è possibile applicare rigorosamente un'implementazione asincrona, il nome implica che dovrebbe essere. Se le circostanze cambiano e potrebbe essere una chiamata sincrona per qualsiasi motivo, allora potresti dover considerare un cambio di nome, quindi il mio consiglio sarebbe di farlo solo se non pensi che questo sarà molto probabile nel futuro.


0

Ecco un punto di vista opposto.

Non siamo passati dal tornare Fooal ritorno Task<Foo>perché abbiamo iniziato a desiderare il Taskinvece del solo Foo. Certo, a volte interagiamo con il Taskcodice ma nella maggior parte del mondo reale lo ignoriamo e usiamo semplicemente il Foo.

Inoltre, spesso definiamo interfacce per supportare il comportamento asincrono anche quando l'implementazione può essere o meno asincrona.

In effetti un'interfaccia che restituisce a Task<Foo>ti dice che l'implementazione è probabilmente asincrona, indipendentemente dal fatto che lo sia davvero, anche se potrebbe interessarti o meno. Se un'astrazione ci dice più di quanto abbiamo bisogno di sapere sulla sua implementazione, perde.

Se la nostra implementazione non è asincrona, la modifichiamo in asincrona e quindi dobbiamo cambiare l'astrazione e tutto ciò che la utilizza, è un'astrazione che perde molto.

Non è un giudizio. Come altri hanno sottolineato, tutte le astrazioni perdono. Questo ha un impatto maggiore perché richiede un effetto a catena di asincrono / attende in tutto il nostro codice solo perché da qualche parte alla fine potrebbe esserci qualcosa che è effettivamente asincrono.

Sembra una lamentela? Non è questo il mio intento, ma penso che sia un'osservazione accurata.

Un punto correlato è l'affermazione che "un'interfaccia non è un'astrazione". Ciò che Mark Seeman ha brevemente affermato è stato leggermente abusato.

La definizione di "astrazione" non è "interfaccia", nemmeno in .NET. Le astrazioni possono assumere molte altre forme. Un'interfaccia può essere una cattiva astrazione o può rispecchiare la sua implementazione così da vicino che in un certo senso non è affatto un'astrazione.

Ma usiamo assolutamente le interfacce per creare astrazioni. Quindi, lanciare "le interfacce non sono astrazioni" perché una domanda menziona le interfacce e le astrazioni non è illuminante.


-2

In GetAllAsync()realtà è asincrono? Voglio dire che "asincrono" è nel nome, ma che può essere rimosso. Quindi chiedo di nuovo ... È impossibile implementare una funzione che restituisce una Task<IEnumerable<User>>che viene risolta in modo sincrono?

Non conosco i dettagli del Tasktipo .Net , ma se è impossibile implementare la funzione in modo sincrono, allora sicuramente è un'astrazione che perde (in questo modo) ma altrimenti no. Io non so che, se si trattava di una IObservablepiuttosto che un compito, potrebbe essere implementato in modo sincrono o asincrono in modo da niente al di fuori della funzione conosce e quindi non è che perde quel fatto particolare.


Task<T> significa asincrono. Ottieni immediatamente l'oggetto task, ma potrebbe essere necessario attendere la sequenza di utenti
Caleth

Potrebbe dover aspettare non significa che sia necessariamente asincrono. Deve aspettare significherebbe asincrona. Presumibilmente, se l'attività sottostante è già stata eseguita, non è necessario attendere.
Daniel T.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.