Come potrei progettare 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, Id e IsInvalidated . Il fatto che siano di sola lettura, tuttavia, non garantisce di per sé che i loro valori rimarranno costanti.

Diciamo che era mia intenzione chiarire che ...

  • Id rappresenta un valore costante (che può quindi essere tranquillamente memorizzato nella cache), mentre
  • IsInvalidated potrebbe cambiare il suo valore durante la vita di un oggetto IX (e quindi non dovrebbe essere memorizzato nella cache).

Come posso modificare interface IX per rendere tale 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 nome simile IsInvalidated potrebbe esserne influenzato.

    Questo argomento vale solo nei casi in cui il metodo e la proprietà sono denominati in modo simile.

  2. Aumenta questa interfaccia con un evento IsInvalidatedChanged :

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;
    

    La presenza di un evento …Changed per IsInvalidated afferma 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 ci sono molte cose aggiuntive che potrebbero non essere affatto utilizzate.

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

    bool IsInvalidated();
    

    Questo potrebbe essere un cambiamento troppo sottile. Dovrebbe essere un suggerimento che ogni volta un valore viene calcolato ogni volta, cosa che non sarebbe necessaria se fosse una costante. L'argomento MSDN "Scelta tra proprietà e metodi" ha questo da dire al riguardo:

    Do use a method, rather than a property, in the following situations. […] The operation returns a different result each time it is called, even if the parameters do not change.

Che tipo di risposte mi aspetto?

  • Sono più interessato a soluzioni completamente diverse al problema, insieme a una spiegazione su come hanno superato i miei tentativi precedenti.

  • Se i miei tentativi sono logicamente errati o presentano svantaggi significativi non ancora menzionati, in modo tale che rimane una sola soluzione (o nessuna), mi piacerebbe sapere dove ho sbagliato.

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

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

posta stakx 24.08.2013 - 14:32
fonte

4 risposte

2

Preferirei la soluzione 3 su 1 e 2.

Il mio problema con la soluzione 1 è : cosa succede se non c'è il metodo Invalidate ? Assumi un'interfaccia con una proprietà IsValid che restituisce DateTime.Now > MyExpirationDate ; potresti non avere bisogno di un esplicito metodo SetInvalid qui. Cosa succede se un metodo influenza più proprietà? Assumi un tipo di connessione con IsOpen e IsConnected proprietà - entrambi sono influenzati dal metodo Close .

Soluzione 2 : se l'unico punto dell'evento è informare gli sviluppatori che una proprietà con nome simile può restituire valori diversi per ogni chiamata, allora vorrei strongmente consigliarla. Dovresti mantenere le tue interfacce brevi e chiare; inoltre, non tutte le implementazioni potrebbero essere in grado di attivare quell'evento per te. Utilizzerò nuovamente il mio esempio IsValid di cui sopra: dovresti implementare un timer e generare un evento quando raggiungi MyExpirationDate . Dopo tutto, se quell'evento è parte dell'interfaccia pubblica, gli utenti dell'interfaccia si aspettano che quell'evento funzioni.

Ciò detto riguardo a queste soluzioni: non sono male. La presenza di un metodo o di un evento indicherà che una proprietà con nome simile può restituire valori diversi per ogni chiamata. Tutto quello che sto dicendo è che da soli non sono sufficienti a trasmettere sempre quel significato.

Soluzione 3 è ciò che cercherò. Come detto, questo potrebbe funzionare solo per gli sviluppatori C #. Per me come C # -dev, il fatto che IsInvalidated non sia una proprietà trasmette immediatamente il significato di "non è solo una semplice accessoria, qualcosa sta succedendo qui". Questo non vale però per tutti e, come ha sottolineato MainMa, lo stesso framework .NET non è coerente.

Se volessi utilizzare la soluzione 3, ti consiglierei di dichiararla una convenzione e farla seguire da tutto il team. La documentazione è anch'essa essenziale, credo. Secondo me in realtà è piuttosto semplice suggerire di modificare i valori: " restituisce true se il valore è ancora valido ", " indica se la connessione ha già stato aperto "contro" restituisce true se questo oggetto è valido ". Non sarebbe affatto male per essere più esplicito nella documentazione.

Quindi il mio consiglio sarebbe:

Dichiara la soluzione 3 una convenzione e seguila costantemente. Metti in chiaro nella documentazione delle proprietà se una proprietà ha o meno dei valori che cambiano. Gli sviluppatori che lavorano con proprietà semplici presumono correttamente che non cambino (ovvero cambiano solo quando lo stato dell'oggetto viene modificato). Gli sviluppatori che incontrano un metodo che sembra essere una proprietà ( Count() , Length() , IsOpen() ) sanno che qualcosa sta succedendo e (si spera) leggono i documenti del metodo per capire cosa fa esattamente il metodo e come si comporta.

    
risposta data 30.08.2013 - 13:46
fonte
8

Esiste una quarta soluzione: fai affidamento sulla documentazione .

Come fai a sapere che la classe string è immutabile? Semplicemente lo sai, perché hai letto la documentazione MSDN. StringBuilder , d'altra parte, è mutabile, 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 . Ad esempio:

StringBuilder.Length
DateTime.Now

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

Ciò che viene utilizzato coerentemente su .NET Framework stesso è 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 aggiuntivo potrebbe richiedere qualche tempo. In un caso di una proprietà, ci si aspetta un tempo breve : in effetti, la lunghezza può cambiare durante la vita della lista, ma non ci vuole praticamente nulla per restituirla.

Con le classi, solo guardare il codice mostra chiaramente che il valore di una proprietà non cambierà durante la vita dell'oggetto. Ad 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 pattern a un'interfaccia e dato che C # non è sufficientemente espressivo in questo caso, per colmare il divario dovrebbe essere utilizzata la documentazione (inclusa la documentazione XML e i diagrammi UML).

    
risposta data 24.08.2013 - 15:46
fonte
4

I miei 2 centesimi:

Opzione 3: come non-C # -er, non sarebbe un granché; Tuttavia, a giudicare dalla citazione che porterai, dovrebbe essere chiaro ai programmatori C # competenti che questo significa qualcosa. Tuttavia dovresti comunque aggiungere dei documenti.

Opzione 2: a meno che non pianifichi di aggiungere eventi a tutto ciò che può 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 è id , che di solito è considerato immutabile anche in oggetti mutevoli.
  • Non menzionarlo. Le interfacce non sono così brave nel descrivere il comportamento come vorremmo che fossero; In parte, questo perché il linguaggio non ti permette di descrivere cose come "Questo metodo restituirà sempre lo stesso valore per un oggetto specifico" o "Chiamando questo metodo con un argomento x < 0 genererà un'eccezione."
  • Tranne 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 aggiungerlo al metodo. Potresti aver bisogno di alcuni trucchi per ottenere i metodi di implementazione per "ereditarla", e gli utenti potrebbero aver bisogno di leggere il documento per quell'annotazione, ma sarà uno strumento che ti consente di dire esattamente cosa vuoi dire, invece di suggerire a questo.
risposta data 24.08.2013 - 15:19
fonte
3

Un'altra opzione sarebbe quella di dividere l'interfaccia: partendo dal presupposto che dopo aver chiamato Invalidate() ogni invocazione di IsInvalidated debba restituire lo stesso valore true , non sembra esserci alcun motivo per richiamare IsInvalidated per la stessa parte che invocato Invalidate() .

Quindi, suggerisco che controlli per invalidazione o causa , ma sembra irragionevole fare entrambe le cose. Pertanto ha senso offrire un'interfaccia contenente l'operazione Invalidate() alla prima parte e un'altra interfaccia per il controllo ( IsInvalidated ) all'altra. Poiché è abbastanza ovvio dalla firma della prima interfaccia che causerà l'invalidazione dell'istanza, la domanda rimanente (per la seconda interfaccia) è in effetti abbastanza generale: come specificare se il tipo specificato è immutabile .

interface Invalidatable
{
    bool IsInvalidated { get; }
}

Lo so, questo non risponde direttamente alla domanda, ma almeno lo riduce alla domanda su come contrassegnare un tipo immutabile , che è un problema comune e comprensibile. Ad esempio, assumendo che tutti i tipi siano modificabili a meno che non sia definito diversamente, è possibile concludere che la seconda interfaccia ( IsInvalidated ) è modificabile e pertanto può modificare il valore di IsInvalidated di volta in volta. D'altra parte, supponiamo che la prima interfaccia assomigli a questa (pseudo-sintassi):

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

Poiché è contrassegnato come immutabile, sai che l'ID non cambierà. Ovviamente, invocare invalidate farà sì che lo stato dell'istanza cambi, ma questa modifica non sarà osservabile tramite questa interfaccia.

    
risposta data 30.08.2013 - 16:38
fonte

Leggi altre domande sui tag