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 IReservationsRepository
dipendenza è determinata esclusivamente dal client, la MaîtreD
classe:
public interface IReservationsRepository
{
Reservation[] ReadReservations(DateTimeOffset date);
int Create(Reservation reservation);
}
Questa interfaccia è completamente sincrona poiché la MaîtreD
classe 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îtreD
classe, 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 TryAccept
metodo 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.