Prima di tutto, scusa se questo post è troppo lungo. Inizierò con ...
Versione breve:
È generalmente consigliabile o una buona idea progettare una proprietà di interfaccia come asincrona semplicemente perché non possiamo essere sicuri che ogni singola implementazione possa garantire che la sua valutazione sarà a buon mercato? Cioè, può essere una buona idea progettare un'interfaccia come questa:
interface IFoo
{
Task<IEnumerable<Bar>> BarsAsync { get; }
}
invece di questo:
interface IFoo
{
IEnumerable<Bar> Bars { get; }
}
Versione lunga:
Ora lasciami spiegare come sono arrivato a questa domanda di design.
Iniziamo con la versione non asincrona dell'interfaccia (mostrata sopra)
Le linee guida di progettazione della struttura di Microsoft richiedono che il valore di una proprietà è economico; ma quando programmiamo contro un'interfaccia, invece che contro un'implementazione particolare di IFoo
, non possiamo essere certi che ogni implementazione di IFoo
che potremmo utilizzare possa e aderirà a tale linea guida.
Ciò che ciascuna implementazione potrebbe fare è garantire "economicità ammortizzata", cioè memorizzare il valore della proprietà al suo primo calcolo, in modo che almeno gli accessi successivi siano a buon mercato. Eric Lippert scrive nel suo articolo del blog "Quando dovrei scrivere una proprietà?" :
A common pattern is for a property to be a cache of an expensive, lazily-computed value […]. Though the property is expensive on its first access, every subsequent access is cheap, so the property is cheap in an amortized sense.
Prosegue suggerendo di utilizzare Lazy<T>
come dettaglio di implementazione verso tale fine, quindi diamo un'occhiata a una possibile implementazione dell'interfaccia precedente dove valutare Bars
per la prima volta sarebbe costoso, ma non per le chiamate successive:
internal class ExpensiveFoo : IFoo
{
private readonly Lazy<IEnumerable<Bar>> bars;
public IEnumerable<Bar> Bars { get { return bars.Value; } }
public ExpensiveFoo() { bars = new Lazy<IEnumerable<Bar>>(GetBars); }
private IEnumerable<Bar> GetBars() { … /* some expensive computation */ }
}
Ciò che mi chiedo a questo punto è, non sarebbe meglio semplicemente perdere la possibilità di una costosa prima valutazione nell'interfaccia e cambiarla in:
interface IFoo
{
Task<IEnumerable<Bar>> BarsAsync { get; }
}
Ciò ti consentirebbe di scrivere implementazioni sia "sempre a buon mercato" sia implementazioni "a costi ridotti" (come sopra), con la consapevolezza che il consumatore ne è consapevole.
internal class ExpensiveFoo : IFoo
{
private Lazy<Task<IEnumerable<Bar>>> bars;
public Task<IEnumerable<Bar>> BarsAsync { get { return bars.Value; } }
public ExpensiveFoo() { bars = new Lazy<Task<IEnumerable<Bar>>>(GetBarsAsync); }
private async Task<IEnumerable<Bar>> GetBarsAsync() { … };
}
internal class CheapFoo : IFoo
{
public Task<IEnumerable<Bar>> BarsAsync { get { return Task.FromResult(bars) } }
private IEnumerable<Bar> bars;
public CheapFoo(IEnumerable<Bar> bars) { this.bars = bars; }
}
Indipendentemente dall'implementazione utilizzata, è possibile scrivere codice che potenzialmente non si blocca anche sul primo accesso alle proprietà:
Foo foo = …;
foreach (Bar bar in await foo.BarsAsync) …
Questa è sempre una buona idea?