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


31

Abbastanza diretto. Sto implementando un'interfaccia, ma c'è una proprietà che non è necessaria per questa classe e, in effetti, non dovrebbe essere usata. La mia idea iniziale era semplicemente 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? In tal caso, come ti sei avvicinato?


1
Ricordo vagamente che esiste una classe semi-comunemente usata in C # che implementa un'interfaccia ma afferma esplicitamente nella documentazione che un certo metodo non è implementato. Proverò a vedere se riesco a trovarlo.
Mago Xy,

Sarei sicuramente interessato a vederlo, se riesci a trovarlo.
Chris Pratt,

23
Posso sottolineare più casi di questo nella libreria di .NET - E SONO TUTTI RICONOSCIUTI COME ERRORI ERRATI TERRIBILI . Questa è una violazione iconica e comune del principio di sostituzione di Liskov - i motivi per non violare LSP possono essere trovati nella mia risposta qui
Jimmy Hoffa,

3
Ti viene richiesto di implementare questa interfaccia specifica o potresti introdurre una superinterfaccia e usarla?
Christopher Schultz,

6
"una proprietà non necessaria per questa classe" - Se una parte di un'interfaccia è necessaria dipende dai client dell'interfaccia, non dagli implementatori. Se una classe non può ragionevolmente implementare un membro di un'interfaccia, allora la classe non è adatta per l'interfaccia. Ciò può significare che l'interfaccia è mal progettata - probabilmente sta cercando di fare troppo - ma ciò non aiuta la classe.
Sebastian Redl il

Risposte:


51

Questo è un classico esempio di come le persone decidono di violare il principio di sottotitolazione di Liskov. Lo scoraggio fortemente, ma incoraggio 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 secondo il Principio di segregazione dell'interfaccia.

Se il primo è il tuo caso, non implementare l'interfaccia su quella classe. Pensalo come una presa elettrica in cui il foro di terra non è necessario, quindi non si attacca realmente a terra. Non collegare nulla con la terra e nessun grosso problema! Ma non appena usi qualcosa che ha bisogno di un terreno, potresti avere un fallimento spettacolare. Meglio non fare un buco nel terreno falso. Quindi se la tua classe in realtà non fa ciò che l'interfaccia intende, non implementare l'interfaccia.


Ecco alcuni suggerimenti da Wikipedia:

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

Più formalmente, il principio di sostituzione di Liskov (LSP) è una definizione particolare di una sottotipizzazione, chiamata sottotipizzazione comportamentale (forte), che è stata inizialmente introdotta da Barbara Liskov in un discorso programmatico della conferenza del 1987 intitolato Astrazione e gerarchia dei dati. È una relazione semantica piuttosto che semplicemente sintattica perché intende garantire l'interoperabilità semantica dei tipi in una gerarchia , [...]

Per l'interoperabilità e la sostituibilità semantiche tra diverse implementazioni degli stessi contratti, sono necessarie tutte per impegnarsi negli stessi comportamenti.


Il principio di segregazione delle interfacce parla dell'idea che le interfacce dovrebbero essere separate in set coerenti in modo tale da non richiedere un'interfaccia che faccia molte cose disparate quando si desidera una sola struttura. Pensa di nuovo 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'utilizzo per scopi non di riscaldamento. Come una presa elettrica con termostato, le interfacce di grandi dimensioni sono difficili da implementare e difficili da usare.

Il principio di segregazione dell'interfaccia (ISP) afferma che nessun client dovrebbe essere costretto a dipendere da metodi che non utilizza. [1] L'ISP suddivide le interfacce che sono molto grandi in più piccole e più specifiche in modo che i clienti debbano solo conoscere i metodi che li interessano.


Assolutamente. Questo è probabilmente il motivo per cui non mi sembrava giusto, in primo luogo. A volte hai solo bisogno di ricordarti che stai facendo qualcosa di stupido. Grazie.
Chris Pratt,

@ChrisPratt è un errore molto comune da fare, ecco perché i formalismi possono essere preziosi: classificare gli odori di codice aiuta a identificarli più rapidamente e a ricordare le soluzioni precedentemente utilizzate.
Jimmy Hoffa,

4

Mi sembra perfetto, se questa è la tua situazione.

Tuttavia, mi sembra che la tua interfaccia (o il suo utilizzo) sia rotta se una classe derivante non la implementa realmente. Valuta di dividere questa interfaccia.

Disclaimer: questo richiede l'ereditarietà multipla per fare correttamente, e non ho idea se C # lo supporti.


6
C # non supporta l'ereditarietà multipla delle classi, ma la supporta per le interfacce.
mgw854,

2
Penso tu abbia ragione. L'interfaccia è un contratto dopo tutto, e anche se so che questa particolare classe non verrà utilizzata in modo da interrompere qualsiasi cosa utilizzi l'interfaccia se questa proprietà è disabilitata, non è ovvio.
Chris Pratt,


4

Mi sono imbattuto in questa situazione. In effetti, come sottolineato altrove, il BCL ha tali esempi ... Cercherò di fornire esempi migliori e fornire alcune motivazioni:

Quando hai un'interfaccia già spedita che conservi per motivi di compatibilità e ...

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

  • L'interfaccia contiene membri che sono documentati come facoltativi e che l'implementazione potrebbe generare l'eccezione. Ad esempio su MSDN per IEnumerator.Resetquanto riguarda esso dice:

Il metodo di ripristino è fornito per l'interoperabilità COM. Non deve necessariamente essere implementato; invece, l'implementatore può semplicemente lanciare NotSupportedException.

  • A causa di un errore di progettazione dell'interfaccia, in primo luogo avrebbe dovuto essere più di un'interfaccia. È un modello comune in BCL per implementare versioni di sola lettura di contenitori con NotSupportedException. L'ho fatto da solo, è quello che ci si aspetta ora ... Faccio ICollection<T>.IsReadOnlyritorno in truemodo da poter dire loro appart. La progettazione corretta sarebbe stata quella di 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 oggetti per indice, controllare se contiene un oggetto 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 un array che come un elenco. Ma non c'è IArray<T>nel BCL .

  • L'interfaccia appartiene a un'API trasferita su più piattaforme e nell'implementazione di una determinata piattaforma alcune parti di essa 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 chiamate, è assolutamente appropriato ottenerle NotSupportedException. Ciò è particolarmente vero se si tratta di una porta per una nuova piattaforma che non era prevista nel progetto originale.


Considera anche perché non è supportato?

A volte InvalidOperationExceptionè un'opzione migliore. Ad esempio, un altro modo per aggiungere polimorfismo in una classe consiste nell'avere varie implementazioni di un'interfaccia interna e il codice sta scegliendo quale istanziare in base ai parametri forniti nel costruttore della classe. [Ciò è particolarmente utile se si sa che l'insieme di opzioni è fisso e non si desidera consentire l'introduzione di classi di terze parti tramite l'iniezione delle dipendenze.] L'ho fatto per eseguire il backport di ThreadLocal perché l'implementazione di tracciamento e non tracciamento è troppo lontano appart, e cosa getta sull'implementazione senza tracciamento? anche se non dipende da lo stato dell'oggetto. In questo caso ho introdotto io stesso la classe e sapevo che questo metodo doveva essere implementato semplicemente lanciando un'eccezione.ThreadLocal.ValuesInvalidOperationException

A volte ha un valore predefinito. Ad esempio su quanto ICollection<T>.IsReadOnlysopra menzionato, ha senso restituire semplicemente "vero" o "falso" a seconda del caso. Quindi ... di cosa tratta la semantica IFoo.Bar? forse c'è qualche valore predefinito ragionevole da restituire.


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


0

Qualcun altro ha mai incontrato una situazione simile prima?

Sì, lo hanno fatto i progettisti di librerie .Net. La classe Dictionary fa quello che stai facendo. Utilizza un'implementazione esplicita per nascondere efficacemente * alcuni dei metodi IDictionary. Qui è spiegato meglio , ma per riassumere, al fine di utilizzare i metodi Add, CopyTo o Remove del dizionario che accettano KeyValuePairs, è innanzitutto necessario eseguire il cast dell'oggetto in un IDictionary.

* Non sta "nascondendo" i metodi nel senso stretto della parola "nascondi" come la usa Microsoft . Ma non conosco un termine migliore in questo caso.


... che è terribile.
Corse di leggerezza con Monica il

Perché il downvote?
user2023861

2
Probabilmente perché non hai risposto alla domanda. Ish. Una specie di.
Corse di leggerezza con Monica il

@LightnessRacesinOrbit, ho aggiornato la mia risposta per renderla più chiara. Spero che aiuti l'OP.
user2023861

2
Sembra che mi risponda alla domanda. Il fatto che possa essere una buona o una cattiva idea non sembra rientrare nell'ambito della domanda, ma potrebbe comunque essere utile per indicare le risposte.
Ellesedil,

-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 qualsiasi valore abbia senso nell'implementazione (int.MinValue, int.MaxValue, 1, 42, ecc ...)

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

Generare un'eccezione sembra una cattiva forma.


2
Perché? In che modo è meglio restituire dati errati?
Corse di leggerezza con Monica il

1
Questo rompe LSP: vedi la risposta di Jimmy Hoffa per una spiegazione del perché.

5
Se non ci sono possibili valori di ritorno corretti, è meglio generare un'eccezione piuttosto che restituire un valore errato. Indipendentemente da ciò che fai, il tuo programma non funzionerà correttamente se invoca accidentalmente questa proprietà. Ma se si lancia un'eccezione, sarà ovvio perché non funziona correttamente.
Tanner Swett,

Non so come il valore sarebbe errato quando si implementa l'interfaccia! È la tua implementazione, può essere quello che vuoi.
Jon Raynor il

Cosa succede se il valore corretto era sconosciuto al momento della compilazione?
Andy,
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.