Domanda

Esiste un modello abbastanza comune usato in .NET per testare le capacità di una classe. Qui userò la classe Stream come esempio, ma il problema si applica a tutte le classi che usano questo modello.

Lo schema è fornire una proprietà booleana chiamata CanXXX per indicare che la capacità XXX è disponibile sulla classe. Ad esempio, la classe Stream ha CanRead, CanWrite e CanSeek per indicare che è possibile chiamare i metodi Read, Write e Seek. Se il valore delle proprietà è falso, la chiamata del rispettivo metodo comporterà il lancio di NotSupportedException.

Dalla documentazione MSDN sulla classe di flusso:

  

A seconda dell'origine dati o del repository sottostanti, i flussi potrebbero supportare solo alcune di queste funzionalità. Un'applicazione può interrogare un flusso per le sue capacità usando le proprietà CanRead, CanWrite e CanSeek.

E documentazione per la proprietà CanRead:

  

Se sottoposto a override in una classe derivata, ottiene un valore che indica se lo stream corrente supporta la lettura.

     

Se una classe derivata da Stream non supporta la lettura, le chiamate ai metodi Read, ReadByte e BeginRead generano NotSupportedException.

Vedo un sacco di codice scritto sulla falsariga di quanto segue:

if (stream.CanRead)
{
    stream.Read(…)
}

Nota che non esiste un codice di sincronizzazione, per esempio, per bloccare l'oggetto stream in alcun modo & # 8212; altri thread potrebbero accedervi o oggetti a cui fa riferimento. Non esiste inoltre un codice per catturare NotSupportedException.

La documentazione MSDN non indica che il valore della proprietà non può cambiare nel tempo. In effetti, la proprietà CanSeek diventa false quando lo stream viene chiuso, dimostrando la natura dinamica di queste proprietà. Pertanto, non esiste alcuna garanzia contrattuale che la chiamata a Read () nello snippet di codice sopra riportato non genererà NotSupportedException.

Mi aspetto che ci sia un sacco di codice là fuori che soffre di questo potenziale problema. Mi chiedo come hanno risolto coloro che hanno identificato questo problema. Quali modelli di progettazione sono appropriati qui?

Gradirei anche commenti sulla validità di questo modello (le coppie CanXXX, XXX ()). Per me, almeno nel caso della classe Stream, ciò rappresenta una classe / interfaccia che sta cercando di fare troppo e che dovrebbe essere suddivisa in parti più fondamentali. La mancanza di un contratto stretto e documentato rende impossibili i test e l'attuazione ancora più difficile!

È stato utile?

Soluzione

Senza conoscere gli interni di un oggetto, devi presumere che una "bandiera" la proprietà è troppo volatile per fare affidamento quando l'oggetto viene modificato in più thread.

Ho visto questa domanda più comune sulle raccolte di sola lettura rispetto agli stream, ma ritengo sia un altro esempio dello stesso modello di design e valgono gli stessi argomenti.

Per chiarire, l'interfaccia ICollection in .NET ha la proprietà IsReadOnly, che deve essere utilizzata come indicatore del fatto che la raccolta supporti metodi per modificarne il contenuto. Proprio come i flussi, questa proprietà può cambiare in qualsiasi momento e provoca il lancio di InvalidOperationException o NotSupportedException.

Le discussioni intorno a questo di solito si riducono a:

  • Perché non esiste invece un'interfaccia IReadOnlyCollection?
  • Se NotSupportedException è una buona idea.
  • Pro e contro di avere " mode " rispetto a funzionalità concrete distinte.

Le modalità sono raramente una buona cosa, poiché sei costretto a gestire più di un "set". di comportamento; avere qualcosa che può cambiare modalità in qualsiasi momento è considerevolmente peggiore, poiché la tua applicazione ora ha a che fare con più di un "set". anche di comportamento. Tuttavia, solo perché è possibile scomporre qualcosa in una funzionalità più discreta non significa necessariamente che si dovrebbe sempre, in particolare quando la suddivisione non fa nulla per ridurre la complessità del compito da svolgere.

La mia opinione personale è che devi scegliere il modello più vicino al modello mentale che percepisci, capiranno i consumatori della tua classe. Se sei l'unico consumatore, scegli il modello che preferisci. Nel caso di Stream e ICollection, penso che avere una sola definizione di questi sia molto più vicino al modello mentale costruito da anni di sviluppo in sistemi simili. Quando parli di flussi, parli di flussi di file e flussi di memoria, non che siano leggibili o scrivibili. Allo stesso modo, quando parli di collezioni, raramente ti riferisci ad esse in termini di "scrivibilità".

La mia regola empirica su questo: cerca sempre un modo per suddividere i comportamenti in interfacce più specifiche, piuttosto che avere "modalità". di funzionamento, purché complimenti un semplice modello mentale. Se è difficile pensare ai comportamenti separati come cose separate, usa un modello basato sulla modalità e documentalo chiaramente molto .

Altri suggerimenti

Va ??bene, ecco un altro tentativo che si spera sarà più utile della mia altra risposta ...

È un peccato che MSDN non fornisca garanzie specifiche su come CanRead / CanWrite / CanSeek può cambiare nel tempo. Penso che sarebbe ragionevole presumere che se uno stream è leggibile continuerà a essere leggibile fino a quando non verrà chiuso - e lo stesso vale per le altre proprietà

In alcuni casi penso che sarebbe ragionevole per un diventare ricercabile in un secondo momento - ad esempio, potrebbe bufferizzare tutto ciò che legge fino a raggiungere la fine dei dati sottostanti, e quindi consentire la ricerca al suo interno in seguito per consentire ai clienti di rileggere i dati. Penso che sarebbe ragionevole per un adattatore ignorare questa possibilità, tuttavia.

Questo dovrebbe occuparsi di tutti tranne i casi più patologici. (I flussi sono progettati per provocare il caos!) L'aggiunta di questi requisiti alla documentazione esistente è un cambiamento teoricamente rivoluzionario, anche se sospetto che il 99,9% delle implementazioni lo obbedirà già. Tuttavia, potrebbe valere la pena suggerire su Connect .

Ora, per quanto riguarda la discussione tra se usare un "basato sulle capacità" API (come Stream ) e basata sull'interfaccia ... il problema fondamentale che vedo è che .NET non fornisce la possibilità di specificare che una variabile deve essere un riferimento a un'implementazione di più di un'interfaccia. Ad esempio, non riesco a scrivere:

public static Foo ReadFoo(IReadable & ISeekable stream)
{
}

Se lo ha permesso , potrebbe essere ragionevole - ma senza quello, si finisce con un'esplosione di potenziali interfacce:

IReadable
IWritable
ISeekable
IReadWritable
IReadSeekable
IWriteSeekable
IReadWriteSeekable

Penso che sia più disordinato della situazione attuale, anche se penso che sosterrei l'idea di solo IReadable e IWritable oltre al classe Stream esistente. Ciò renderebbe più semplice per i clienti l'espressione dichiarativa di ciò di cui hanno bisogno.

Con Contratti di codice , le API possono dichiarare cosa forniscono e cosa richiedono, certamente:

public Stream OpenForReading(string name)
{
    Contract.Ensures(Contract.Result<Stream>().CanRead);

    ...
}

public void ReadFrom(Stream stream)
{
    Contract.Requires(stream.CanRead);

    ...
}

Non so quanto il controllore statico possa aiutare in questo - o come affronti il ??fatto che i flussi fanno diventano illeggibili / non scrivibili quando sono chiusi.

stream.CanRead controlla solo se lo stream sottostante ha la possibilità di leggere. Non dice nulla sulla possibile lettura effettiva (ad es. Errore del disco).

Non è necessario catturare NotImplementedException se si utilizza una delle classi * Reader poiché tutte supportano la lettura. Solo * Writer avrà CanRead = False e genererà tale eccezione. Se sei a conoscenza del fatto che stream supporta la lettura (ad esempio hai utilizzato StreamReader), IMHO non è necessario effettuare ulteriori controlli.

È comunque necessario rilevare le eccezioni poiché qualsiasi errore durante la lettura le genererà (ad es. errore del disco).

Si noti inoltre che qualsiasi codice non documentato come thread-safe non è thread-safe. Di solito i membri statici sono thread-safe, ma i membri dell'istanza no - tuttavia, è necessario controllare la documentazione per ogni classe.

Dalla tua domanda e da tutti i commenti successivi, immagino che il tuo problema sia con la chiarezza e la "correttezza". del contratto dichiarato. Il contratto dichiarato è quello che è nella documentazione online MSDN.

Quello che hai sottolineato è che manca qualcosa nella documentazione che costringe a fare ipotesi sul contratto. Più specificamente, poiché non si dice nulla sulla volatilità della proprietà di leggibilità di uno stream, l'unica ipotesi che si può fare è che è possibile che un NotSupportedException sia generato, indipendentemente da quale sia stato il valore della proprietà CanRead corrispondente pochi millisecondi (o più) prima.

Penso che si debba andare sull' intento di questa interfaccia in questo caso, cioè:

  1. Se usi più di un thread, tutte le scommesse sono disattivate;
  2. fino a quando non vai a chiamare qualcosa sull'interfaccia che cambierà potenzialmente lo stato del flusso, puoi tranquillamente supporre che il valore di CanRead sia invariante.

Nonostante quanto sopra, i metodi Read * possono potenzialmente generare un NotSupportedException .

Lo stesso argomento può essere applicato a tutte le altre proprietà Can *.

  

Gradirei anche commenti sulla validità di questo modello (le coppie CanXXX, XXX ()).

Quando vedo un'istanza di questo modello, generalmente mi aspetto questo:

  1. Un membro CanXXX senza parametri restituirà sempre lo stesso valore, a meno che & # 8230;

  2. & # 8230; in presenza di un CanXXXChanged evento , in cui un CanXXX senza parametri può restituire un valore diverso prima e dopo il verificarsi di tale evento; ma non cambierà senza attivare l'evento.

  3. Un membro CanXXX(…) parametrizzato può restituire valori diversi per argomenti diversi; ma per gli stessi argomenti, è probabile che restituisca lo stesso valore. Cioè, CanXXX (constValue) probabilmente rimarrà costante.

      

    Sono cauto qui: se stream.CanWriteToDisk (largeConstObject) restituisce true ora, è ragionevole supporre che restituirà sempre true in futuro? Probabilmente no, quindi forse dipende dal contesto se un CanXXX (& # 8230;) con parametri restituirà o meno lo stesso valore per gli stessi argomenti.

  4. Una chiamata a XXX(…) può avere successo solo se CanXXX restituisce true .


Detto questo, sono d'accordo che l'uso di Stream di questo modello sia alquanto problematico. Almeno in teoria, forse forse non tanto in pratica.

Sembra più un problema teorico che pratico. Non riesco davvero a pensare a situazioni in cui un flusso diventerebbe illeggibile / non scrivibile diverso se non a causa della sua chiusura.

Potrebbero esserci casi angolari, ma non mi aspetto che si presentino spesso. Non credo che la stragrande maggioranza del codice debba preoccuparsi di questo.

È comunque un interessante problema filosofico.

MODIFICA: Affrontando la questione se CanRead ecc. sia utile, credo che lo siano ancora - principalmente per la convalida dell'argomento. Ad esempio, solo perché un metodo prende un flusso che vorrà leggere ad un certo punto non significa che vuole leggerlo proprio all'inizio del metodo, ma è qui che idealmente dovrebbe essere eseguita la convalida dell'argomento. Questo non è in realtà diverso dal verificare se un parametro è nullo e dal lanciare ArgumentNullException invece di aspettare che venga lanciato un NullReferenceException quando ti capita di dereferenziarlo.

Inoltre, CanSeek è leggermente diverso: in alcuni casi il tuo codice potrebbe far fronte a flussi sia cercabili che non ricercabili, ma con maggiore efficienza nel caso ricercabile.

Questo si basa sulla "ricercabilità" ecc., rimanendo coerenti, ma come ho già detto, sembra vero nella vita reale.


Ok, proviamo a metterlo in un altro modo ...

A meno che tu non stia leggendo / cercando nella memoria e non ti sia già assicurato che ci siano abbastanza dati o che tu stia scrivendo all'interno di un buffer preallocato, c'è sempre la possibilità che qualcosa vada storto. I dischi si guastano o si riempiono, le reti collassano ecc. Queste cose fanno accadono nella vita reale, quindi è sempre necessario codificare in modo da sopravvivere al fallimento (o scegliere consapevolmente di ignorare il problema quando non lo fa ' importa davvero).

Se il tuo codice può fare la cosa giusta in caso di guasto del disco, è probabile che sopravviva a un FileStream passando da scrivibile a non scrivibile.

Se Stream avesse contratti saldi, dovrebbero essere incredibilmente deboli - non potresti usare il controllo statico per provare che il tuo codice funzionerà sempre. Il meglio che puoi fare è dimostrare che ha fatto la cosa giusta di fronte al fallimento.

Non credo che Stream cambierà presto. Anche se certamente accetto che potrebbe essere meglio documentato, non accetto l'idea che sia "completamente rotto". Sarebbe più rotto se non potessimo effettivamente usarlo nella vita reale ... e se potesse essere più rotto di quanto non sia ora, logicamente non è completamente rotto .

Ho problemi molto più grandi con il framework, come le API data / ora relativamente scadenti. Sono diventati lotto migliori nelle ultime due versioni, ma mancano ancora molte funzionalità di (diciamo) Joda Time . La mancanza di raccolte immutabili incorporate, scarso supporto per l'immutabilità nella lingua, ecc. - Questi sono problemi reali che mi causano mal di testa reali . Preferirei vederli indirizzati piuttosto che passare anni su Stream che mi sembra un problema teorico alquanto intrattabile che causa pochi problemi nella vita reale.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top