Implementazione di un'interfaccia quando non è necessaria una delle proprietà

27

Piuttosto semplice. Sto implementando un'interfaccia, ma c'è una proprietà che non è necessaria per questa classe e, infatti, non dovrebbe essere utilizzata. La mia idea iniziale era di fare qualcosa del tipo:

int IFoo.Bar
{
    get { raise new NotImplementedException(); }
}

Suppongo che non ci sia nulla di sbagliato in questo, di per sé, ma non sembra "giusto". Qualcun altro ha mai incontrato una situazione simile prima? Se sì, come ti sei accostato?

    
posta Chris Pratt 29.12.2015 - 18:22
fonte

5 risposte

46

Questo è un classico esempio di come le persone decidono di violare il Principio di sottotitolazione di Liskov. Lo sconsiglio vivamente, ma incoraggerei forse una soluzione diversa:

  1. Forse la classe che stai scrivendo non fornisce la funzionalità che l'interfaccia prescrive se non ha l'uso di tutti i membri dell'interfaccia.
  2. In alternativa, quell'interfaccia potrebbe fare più cose e potrebbe essere separata per il principio di segregazione dell'interfaccia.

Se il primo è il tuo caso, non implementare l'interfaccia su quella classe. Pensa a una presa elettrica in cui il foro di terra non è necessario, quindi non in realtà si attacca al terreno. Non si collega nulla con la terra e non un grosso problema! Ma non appena usi qualcosa che ha bisogno di un terreno, potresti subire un fallimento spettacolare. È meglio non punzecchiare un foro falso. Quindi se la tua classe non effettivamente fa ciò che l'interfaccia intende, non implementare l'interfaccia.

Ecco alcuni bit rapidi di wikipedia:

Principio di sostituzione di Liskov può essere semplicemente formulato come "Non rafforzare le pre-condizioni e non indebolire post-condizioni".

More formally, the Liskov substitution principle (LSP) is a particular definition of a subtyping relation, called (strong) behavioral subtyping, that was initially introduced by Barbara Liskov in a 1987 conference keynote address entitled Data abstraction and hierarchy. It is a semantic rather than merely syntactic relation because it intends to guarantee semantic interoperability of types in a hierarchy, [...]

Per l'interoperabilità semantica e la sostituibilità tra diverse implementazioni degli stessi contratti - hai bisogno che tutti si impegnino per gli stessi comportamenti.

Principio di segregazione dell'interfaccia parla dell'idea che le interfacce devono essere separate in insiemi coerenti in modo tale da non richiedere un'interfaccia che fa molte cose disparate quando si desidera solo una funzione one . Pensa ancora all'interfaccia di una presa elettrica, potrebbe avere anche un termostato, ma renderebbe più difficile l'installazione di una presa elettrica e potrebbe rendere più difficile l'uso per scopi non di riscaldamento. Come una presa elettrica con un termostato, le interfacce di grandi dimensioni sono difficili da implementare e difficili da usare.

The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.[1] ISP splits interfaces which are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.

    
risposta data 29.12.2015 - 22:03
fonte
4

Per me va bene, se questa è la tua situazione.

Tuttavia, mi sembra che la tua interfaccia (o il suo uso) sia interrotta se una classe derivante non lo implementa realmente. Prendi in considerazione la possibilità di suddividere l'interfaccia.

Dichiarazione di non responsabilità: Ciò richiede che l'ereditarietà multipla funzioni correttamente e non ho idea se C # lo supporti.

    
risposta data 29.12.2015 - 18:28
fonte
4

Mi sono imbattuto in questa situazione. Infatti, come indicato in altrove , il BCL presenta tali esempi ... Proverò a fornire esempi migliori e a fornire qualche razionale:

Quando hai un'interfaccia già fornita che mantieni per ragioni di compatibilità e ...

  • L'interfaccia contiene membri obsoleti o scomposti. Ad esempio BlockingCollection<T>.ICollection.SyncRoot (tra gli altri) mentre ICollection.SyncRoot non è obsoleto di per sé, getterà NotSupportedException .

  • L'interfaccia contiene membri documentati come facoltativi e che l'implementazione può generare un'eccezione. Ad esempio su MSDN riguardo IEnumerator.Reset dice:

The Reset method is provided for COM interoperability. It does not necessarily need to be implemented; instead, the implementer can simply throw a NotSupportedException.

  • Da un errore del design dell'interfaccia, avrebbe dovuto essere più di un'interfaccia in primo luogo. È un modello comune in BCL per implementare le versioni di sola lettura dei contenitori con NotSupportedException . L'ho fatto io stesso, è ciò che è previsto ora ... Rendo ICollection<T>.IsReadOnly return true in modo da poterli dire appart. Il design corretto avrebbe dovuto avere una versione leggibile dell'interfaccia, e quindi l'intera interfaccia eredita da quella.

  • Non esiste un'interfaccia migliore da usare. Ad esempio, ho una classe che ti consente di accedere agli elementi per indice, controllare se contiene un elemento e su quale indice, ha delle dimensioni, puoi copiarlo in un array ... sembra un lavoro per IList<T> ma la mia classe ha una dimensione fissa, e non supporta l'aggiunta o la rimozione, quindi funziona più come una matrice che come una lista. Ma c'è no IArray<T> nel BCL .

  • L'interfaccia appartiene a un'API che viene portata su più piattaforme e nell'implementazione di una particolare piattaforma alcune parti non sono supportate. Idealmente ci sarebbe un modo per rilevarlo, in modo che il codice portatile che utilizza tale API possa decidere qualunque cosa chiamare o meno quelle parti ... ma se le chiami, è del tutto appropriato ottenere NotSupportedException . Questo è particolarmente vero, se si tratta di una porta su una nuova piattaforma che non era prevista nel progetto originale.

Considerare anche perché non è supportato?

A volte InvalidOperationException è un'opzione migliore. Ad esempio, un altro modo per aggiungere il polimorfismo in una classe è avere varie implementazioni di un'interfaccia interna e il tuo codice sta scegliendo quale istanziare a seconda dei parametri forniti nel costruttore della classe. [Questo è particolarmente utile se si sa che il set di opzioni è fisso e non si desidera consentire l'introduzione di classi di terze parti tramite l'iniezione delle dipendenze.] Ho fatto ciò a backport ThreadLocal perché l'implementazione di tracciamento e non-tracciamento sono troppo lontani e cosa fa ThreadLocal.Values su l'implementazione non di tracciamento? InvalidOperationException anche tho, non dipende dallo stato dell'oggetto. In questo caso ho introdotto la classe da solo, e sapevo che questo metodo doveva essere implementato semplicemente lanciando un'eccezione.

A volte un valore predefinito ha un senso. Ad esempio sul ICollection<T>.IsReadOnly menzionato sopra, ha senso restituire solo "vero" o "falso" a seconda del caso. Quindi ... qual è la semantica di IFoo.Bar ? esiste forse un valore predefinito ragionevole da restituire.

Addendum: se hai il controllo dell'interfaccia (e non hai bisogno di stare con esso per la compatibilità) non dovrebbe esserci un caso in cui devi lanciare NotSupportedException . Sebbene, potrebbe essere necessario dividere l'interfaccia in due o più interfacce più piccole per adattarsi al caso, il che potrebbe portare a "inquinamento" nelle situazioni estreme.

    
risposta data 30.12.2015 - 09:05
fonte
0

Has anyone else come across a similar situation before?

Sì, i progettisti della libreria .NET hanno fatto. La classe Dizionario fa quello che stai facendo. Usa un'implementazione esplicita per nascondere * alcuni dei metodi IDictionary in modo efficace. È spiegato meglio qui , ma per riassumere , per utilizzare i metodi Aggiungi, Copia o Rimuovi dal dizionario che utilizzano KeyValuePairs, devi prima eseguire il cast dell'oggetto su un IDictionary.

* Non "nasconde" i metodi nel senso stretto della parola "nascondi" come Microsoft lo utilizza . Ma in questo caso non conosco un termine migliore.

    
risposta data 29.12.2015 - 21:18
fonte
-6

Potresti sempre implementare e restituire un valore benigno o un valore predefinito. Dopotutto è solo una proprietà. Potrebbe restituire 0 (la proprietà predefinita di int), o qualunque valore abbia senso nella tua implementazione (int.MinValue, int.MaxValue, 1, 42, ecc ...)

//We don't officially implement this property
int IFoo.Bar
{
     { get; }
}

Lanciare un'eccezione sembra una brutta forma.

    
risposta data 29.12.2015 - 22:58
fonte

Leggi altre domande sui tag