Come progetterei un'interfaccia in modo tale che sia chiaro quali proprietà possono cambiare il loro valore e quali rimarranno costanti?


12

Sto riscontrando un problema di progettazione relativo alle proprietà .NET.

interface IX
{
    Guid Id { get; }
    bool IsInvalidated { get; }
    void Invalidate();
}

Problema:

Questa interfaccia ha due proprietà di sola lettura Ide IsInvalidated. Il fatto che siano di sola lettura, tuttavia, non garantisce di per sé che i loro valori rimangano costanti.

Diciamo che era mia intenzione chiarire che ...

  • Id rappresenta un valore costante (che può quindi essere memorizzato nella cache in modo sicuro), mentre
  • IsInvalidatedpotrebbe cambiare il suo valore durante la vita di un IXoggetto (e quindi non dovrebbe essere memorizzato nella cache).

Come potrei modificare interface IXper rendere quel contratto sufficientemente esplicito?

I miei tre tentativi di soluzione:

  1. L'interfaccia è già ben progettata. La presenza di un metodo chiamato Invalidate()consente a un programmatore di dedurre che il valore della proprietà con un nome simile IsInvalidatedpotrebbe esserne influenzato.

    Questo argomento vale solo nei casi in cui il metodo e la proprietà hanno nomi simili.

  2. Aumenta questa interfaccia con un evento IsInvalidatedChanged:

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;
    

    La presenza di un …Changedevento per IsInvalidatedafferma che questa proprietà può cambiare il suo valore e l'assenza di un evento simile per Idè una promessa che quella proprietà non cambierà il suo valore.

    Mi piace questa soluzione, ma è un sacco di roba aggiuntiva che potrebbe non essere utilizzata affatto.

  3. Sostituisci la proprietà IsInvalidatedcon un metodo IsInvalidated():

    bool IsInvalidated();

    Questo potrebbe essere un cambiamento troppo sottile. Dovrebbe essere un indizio del fatto che un valore viene calcolato di fresco ogni volta, il che non sarebbe necessario se fosse una costante. L'argomento MSDN "Scegliere tra proprietà e metodi" ha questo da dire al riguardo:

    Utilizzare un metodo, anziché una proprietà, nelle seguenti situazioni. […] L'operazione restituisce un risultato diverso ogni volta che viene chiamata, anche se i parametri non cambiano.

Che tipo di risposte mi aspetto?

  • Sono molto interessato a soluzioni completamente diverse al problema, insieme a una spiegazione su come hanno battuto i miei tentativi di cui sopra.

  • Se i miei tentativi sono logicamente imperfetti o presentano svantaggi significativi non ancora menzionati, in modo tale che rimanga solo una soluzione (o nessuna), vorrei sapere dove ho sbagliato.

    Se i difetti sono minori e più di una soluzione rimane dopo averla presa in considerazione, si prega di commentare.

  • Per lo meno, vorrei un feedback su quale sia la tua soluzione preferita e per quale motivo.


Non ha IsInvalidatedbisogno di un private set?
James,

2
@James: Non necessariamente, né nella dichiarazione dell'interfaccia, né in un'implementazione:class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
stakx

@James Le proprietà nelle interfacce non sono le stesse delle proprietà automatiche, anche se usano la stessa sintassi.
svick,

Mi chiedevo: le interfacce "hanno il potere" di imporre un simile comportamento? Questo comportamento non dovrebbe essere delegato a ciascuna implementazione? Penso che se vuoi imporre le cose a quel livello, dovresti considerare di creare una classe astratta in modo che i sottotipi ereditino da essa, invece di implementare una data interfaccia. (a proposito, la mia logica ha senso?)
heltonbiker,

@heltonbiker Sfortunatamente, la maggior parte delle lingue (lo so) non consente di esprimere tali vincoli sui tipi, ma sicuramente dovrebbero . Questa è una questione di specifica, non di implementazione. Nella mia opzione, ciò che manca è la possibilità di esprimere gli invarianti di classe e post-condizione direttamente nella lingua. Idealmente, non dovremmo mai preoccuparci di InvalidStateExceptions, per esempio, ma non sono sicuro che ciò sia anche teoricamente possibile. Sarebbe carino, però.
proskor,

Risposte:


2

Preferirei la soluzione 3 rispetto a 1 e 2.

Il mio problema con la soluzione 1 è : cosa succede se non esiste un Invalidatemetodo? Assumi un'interfaccia con una IsValidproprietà che ritorni DateTime.Now > MyExpirationDate; potresti non aver bisogno di un SetInvalidmetodo esplicito qui. Cosa succede se un metodo influenza più proprietà? Assumi un tipo di connessione con IsOpene IsConnectedproperties - entrambi sono interessati dal Closemetodo.

Soluzione 2 : se l'unico punto dell'evento è informare gli sviluppatori che una proprietà con un nome simile può restituire valori diversi su ogni chiamata, allora vi consiglio caldamente di non farlo. Dovresti mantenere le tue interfacce brevi e chiare; inoltre, non tutte le implementazioni potrebbero essere in grado di generare quell'evento per te. Riutilizzerò il mio IsValidesempio dall'alto: dovresti implementare un timer e lanciare un evento quando raggiungi MyExpirationDate. Dopotutto, se quell'evento fa parte della tua interfaccia pubblica, gli utenti dell'interfaccia si aspetteranno che l'evento funzioni.

Detto questo su queste soluzioni: non sono male. La presenza di un metodo o di un evento indica che una proprietà con un nome simile può restituire valori diversi su ogni chiamata. Tutto quello che sto dicendo è che da soli non bastano per trasmettere sempre quel significato.

La soluzione 3 è quella che mi piacerebbe. Come già detto, questo potrebbe funzionare solo per gli sviluppatori C #. Per me come C # -dev, il fatto che IsInvalidatednon sia una proprietà trasmette immediatamente il significato di "che non è solo un semplice accessore, qualcosa sta succedendo qui". Questo non si applica a tutti però, e come sottolineato da MainMa, il framework .NET stesso non è coerente qui.

Se si desidera utilizzare la soluzione 3, consiglierei di dichiararla una convenzione e di seguirla da parte di tutto il team. Anche la documentazione è essenziale, credo. Secondo me in realtà è abbastanza semplice accennare alla modifica dei valori: " restituisce vero se il valore è ancora valido ", " indica se la connessione è già stata aperta " vs. " restituisce vero se questo oggetto è valido ". Non sarebbe affatto male però essere più esplicito nella documentazione.

Quindi il mio consiglio sarebbe:

Dichiarare la soluzione 3 una convenzione e seguirla costantemente. Spiegare chiaramente nella documentazione delle proprietà se una proprietà ha o meno dei valori che cambiano. Gli sviluppatori che lavorano con proprietà semplici assumeranno correttamente che non cambiano (cioè cambiano solo quando lo stato dell'oggetto viene modificato). Gli sviluppatori incontrando un metodo che suona come potrebbe essere una proprietà ( Count(), Length(), IsOpen()) sanno che someting sta succedendo e (si spera) leggere la documentazione metodo per capire che cosa esattamente il metodo fa e come si comporta.


Questa domanda ha ricevuto ottime risposte, ed è un peccato che possa accettarne solo una. Ho scelto la tua risposta perché fornisce un buon riassunto di alcuni punti sollevati nelle altre risposte e perché è più o meno ciò che ho deciso di fare. Grazie a tutti coloro che hanno pubblicato una risposta!
stakx,

8

Esiste una quarta soluzione: fare affidamento sulla documentazione .

Come fai a sapere che la stringclasse è immutabile? Lo sai semplicemente perché hai letto la documentazione MSDN. StringBuilderd'altra parte, è mutevole, perché, ancora una volta, la documentazione lo dice.

/// <summary>
/// Represents an index of an element stored in the database.
/// </summary>
private interface IX
{
    /// <summary>
    /// Gets the immutable globally unique identifier of the index, the identifier being
    /// assigned by the database when the index is created.
    /// </summary>
    Guid Id { get; }

    /// <summary>
    /// Gets a value indicating whether the index is invalidated, which means that the
    /// indexed element is no longer valid and should be reloaded from the database. The
    /// index can be invalidated by calling the <see cref="Invalidate()" /> method.
    /// </summary>
    bool IsInvalidated { get; }

    /// <summary>
    /// Invalidates the index, without forcing the indexed element to be reloaded from the
    /// database.
    /// </summary>
    void Invalidate();
}

La tua terza soluzione non è male, ma .NET Framework non la segue . Per esempio:

StringBuilder.Length
DateTime.Now

sono proprietà, ma non aspettarti che rimangano costanti ogni volta.

Ciò che viene costantemente utilizzato in .NET Framework è questo:

IEnumerable<T>.Count()

è un metodo, mentre:

IList<T>.Length

è una proprietà. Nel primo caso, fare cose potrebbe richiedere un lavoro aggiuntivo, incluso, ad esempio, l'interrogazione del database. Questo lavoro supplementare potrebbe richiedere un po ' di tempo. Nel caso di una proprietà, ci si aspetta che impieghi un po 'di tempo: in effetti, la lunghezza può cambiare durante la vita della lista, ma non ci vuole praticamente nulla per restituirla.


Con le classi, basta guardare il codice per mostrare chiaramente che il valore di una proprietà non cambierà durante la vita dell'oggetto. Per esempio,

public class Product
{
    private readonly int price;

    public Product(int price)
    {
        this.price = price;
    }

    public int Price
    {
        get
        {
            return this.price;
        }
    }
}

è chiaro: il prezzo rimarrà lo stesso.

Purtroppo, non è possibile applicare lo stesso modello a un'interfaccia e dato che C # non è abbastanza espressivo in tal caso, la documentazione (compresi la documentazione XML e i diagrammi UML) dovrebbe essere utilizzata per colmare il divario.


Anche la linea guida "nessun lavoro aggiuntivo" non viene utilizzata in modo assolutamente coerente. Ad esempio, Lazy<T>.Valuepotrebbe richiedere molto tempo per il calcolo (quando si accede per la prima volta).
svick,

1
A mio avviso, non importa se il framework .NET segue la convenzione della soluzione 3. Mi aspetto che gli sviluppatori siano abbastanza intelligenti da sapere se stanno lavorando con tipi interni o tipi di framework. Gli sviluppatori che non sanno che non leggono i documenti e quindi la soluzione 4 non sarebbe di aiuto. Se la soluzione 3 è implementata come una convenzione azienda / team, credo che non importa se anche altre API la seguano (anche se sarebbe molto apprezzata, ovviamente).
enzi,

4

I miei 2 centesimi:

Opzione 3: come non-C # -er, non sarebbe un indizio; Tuttavia, a giudicare dalla citazione che porti, dovrebbe essere chiaro ai programmatori esperti C # che questo significa qualcosa. Dovresti comunque aggiungere documenti su questo.

Opzione 2: a meno che non preveda di aggiungere eventi a qualsiasi cosa possa cambiare, non andare lì.

Altre opzioni:

  • Rinominare l'interfaccia IXMutable, quindi è chiaro che può cambiare il suo stato. L'unico valore immutabile nel tuo esempio è il id, che di solito è considerato immutabile anche negli oggetti mutabili.
  • Per non parlare di questo. Le interfacce non sono così brave a descrivere il comportamento come vorremmo che fossero; In parte, ciò è dovuto al fatto che il linguaggio in realtà non ti consente di descrivere cose come "Questo metodo restituirà sempre lo stesso valore per un oggetto specifico" o "Chiamare questo metodo con un argomento x <0 genererà un'eccezione".
  • Tranne il fatto che esiste un meccanismo per espandere la lingua e fornire informazioni più precise sui costrutti - Annotazioni / Attributi . A volte hanno bisogno di un po 'di lavoro, ma ti permettono di specificare cose arbitrariamente complesse sui tuoi metodi. Trova o inventa un'annotazione che dice "Il valore di questo metodo / proprietà può cambiare per tutta la durata di un oggetto" e aggiungilo al metodo. Potrebbe essere necessario eseguire alcuni trucchi per ottenere i metodi di implementazione per "ereditarla" e gli utenti potrebbero dover leggere il documento per quell'annotazione, ma sarà uno strumento che ti permetterà di dire esattamente quello che vuoi dire, invece di suggerire a esso.

3

Un'altra opzione sarebbe quella di dividere l'interfaccia: supponendo che dopo aver chiamato Invalidate()ogni invocazione IsInvalidateddovrebbe restituire lo stesso valore true, non sembra esserci motivo di invocare IsInvalidatedper la stessa parte che ha invocato Invalidate().

Quindi, suggerisco di controllare l' invalidazione o di causarla , ma sembra irragionevole fare entrambe le cose. Pertanto ha senso offrire un'interfaccia contenente l' Invalidate()operazione alla prima parte e un'altra interfaccia per il controllo ( IsInvalidated) dell'altra. Dal momento che dalla firma della prima interfaccia è abbastanza ovvio che causerà l'invalidazione dell'istanza, la domanda rimanente (per la seconda interfaccia) è davvero piuttosto generale: come specificare se il tipo dato è immutabile .

interface Invalidatable
{
    bool IsInvalidated { get; }
}

Lo so, questo non risponde direttamente alla domanda, ma almeno la riduce alla domanda su come contrassegnare un tipo immutabile , il che è un problema comune e compreso. Ad esempio, supponendo che tutti i tipi siano mutabili, se non diversamente definito, è possibile concludere che la seconda interfaccia ( IsInvalidated) è mutabile e pertanto è possibile modificare il valore di IsInvalidatedvolta in volta. D'altra parte, supponiamo che la prima interfaccia sia simile a questa (pseudo-sintassi):

[Immutable]
interface IX
{
    Guid Id { get; }
    void Invalidate();
}

Poiché è contrassegnato come immutabile, sai che l'Id non cambierà. Naturalmente, la chiamata di invalidate causerà la modifica dello stato dell'istanza, ma questa modifica non sarà osservabile attraverso questa interfaccia.


Non è una cattiva idea! Tuttavia, direi che è sbagliato marcare un'interfaccia [Immutable]fintanto che comprende metodi il cui unico scopo è quello di causare una mutazione. Sposterei il Invalidate()metodo Invalidatablesull'interfaccia.
stakx,

1
@stakx Non sono d'accordo. :) Penso che tu confonda i concetti di immutabilità e purezza (assenza di effetti collaterali). In effetti, dal punto di vista dell'interfaccia proposta IX, non vi è alcuna mutazione osservabile. Ogni volta che chiami Id, otterrai lo stesso valore ogni volta. Lo stesso vale per Invalidate()(non ottieni nulla, dal momento che il suo tipo di ritorno è void). Pertanto, il tipo è immutabile. Certo, avrai effetti collaterali , ma non sarai in grado di osservarli attraverso l'interfaccia IX, quindi non avranno alcun impatto sui client di IX.
proskor,

OK, possiamo essere d'accordo sul fatto che IXè immutabile. E che sono effetti collaterali; sono semplicemente distribuiti tra due interfacce divise in modo tale che i clienti non debbano preoccuparsi (nel caso di IX) o che sia ovvio quale potrebbe essere l'effetto collaterale (nel caso di Invalidatable). Ma perché non passare Invalidatea Invalidatable? Ciò renderebbe IXimmutabile e puro, e Invalidatablenon diventerebbe più mutevole, né più impuro (perché è già entrambi).
stakx,

(Capisco che in uno scenario del mondo reale, la natura della scissione dell'interfaccia probabilmente dipendere più casi di utilizzo pratico che su questioni tecniche, ma sto cercando di capire appieno il vostro ragionamento.)
stakx

1
@stakx Prima di tutto, hai chiesto ulteriori possibilità per rendere più esplicita la semantica della tua interfaccia. La mia idea era di ridurre il problema a quello dell'immutabilità, che richiedeva la divisione dell'interfaccia. Ma non solo era la scissione necessaria per fare uno immutabili interfaccia, sarebbe anche rendere grande senso, perché non v'è alcuna ragione per invocare sia Invalidate()e IsInvalidateddallo stesso consumatore dell'interfaccia . Se chiami Invalidate(), dovresti sapere che viene invalidato, quindi perché controllarlo? In questo modo è possibile fornire due interfacce distinte per scopi diversi.
proskor,
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.