Un'interfaccia è considerata "vuota" se eredita da altre interfacce?


12

Le interfacce vuote sono generalmente considerate cattive pratiche, per quanto ne so, specialmente dove cose come gli attributi sono supportate dal linguaggio.

Tuttavia, un'interfaccia è considerata "vuota" se eredita da altre interfacce?

interface I1 { ... }
interface I2 { ... } //unrelated to I1

interface I3
    : I1, I2
{
    // empty body
}

Qualunque cosa che gli attrezzi I3debbano implementare I1e I2, e gli oggetti di diverse classi che ereditano I3possono quindi essere usati in modo intercambiabile (vedi sotto), quindi è giusto chiamare I3 vuoto ? In caso affermativo quale sarebbe un modo migliore per l'architettura questo?

// with I3 interface
class A : I3 { ... }
class B : I3 { ... }

class Test {
    void foo() {
        I3 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function();
    }
}

// without I3 interface
class A : I1, I2 { ... }
class B : I1, I2 { ... }

class Test {
    void foo() {
        I1 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function(); // we can't do this without casting first
    }
}

1
Non essere in grado di specificare diverse interfacce come tipo di parametro / campo ecc. È un difetto di progettazione in C # / .NET.
CodesInChaos,

Penso che il modo migliore per l'architettura di questa parte dovrebbe andare in una domanda separata. In questo momento la risposta accettata non ha nulla a che fare con il titolo della domanda.
Kapol,

@CodesInChaos No, non è perché ci sono già due modi per farlo. Uno è in questa domanda, l'altro modo sarebbe specificare un parametro di tipo generico per l'argomento del metodo e usare la restrizione where per specificare entrambe le interfacce.
Andy,

Ha senso che una classe che implementa I1 e I2, ma non I3, non possa essere utilizzata da foo? (Se la risposta è "sì", vai avanti!)
user253751

@Andy Anche se non sono pienamente d'accordo con l'affermazione di CodesInChaos secondo cui è un difetto in quanto tale, non penso che i parametri di tipo generico sul metodo siano una soluzione - spinge la responsabilità di conoscere un tipo utilizzato nell'implementazione del metodo su qualunque sta chiamando il metodo, che sembra sbagliato. Forse una sintassi come var : I1, I2 something = new A();per le variabili locali sarebbe piacevole (se ignoriamo i punti positivi sollevati nella risposta di Konrad)
Kai

Risposte:


27

Tutto ciò che implementa I3 dovrà implementare I1 e I2, e gli oggetti di diverse classi che ereditano I3 possono quindi essere usati in modo intercambiabile (vedi sotto), quindi è giusto chiamare I3 vuoto? In caso affermativo quale sarebbe un modo migliore per l'architettura questo?

"Bundling" I1e I2into I3offre una scorciatoia (?) O una sorta di zucchero di sintassi per ovunque tu voglia implementare entrambi.

Il problema con questo approccio è che non puoi impedire ad altri sviluppatori di implementare esplicitamente I1 e I2 fianco a fianco, ma non funziona in entrambi i modi - anche se : I3è equivalente a : I1, I2, : I1, I2non è equivalente a : I3. Anche se funzionalmente identici, una classe che supporta entrambi I1e I2non verrà rilevata come supporto I3.

Ciò significa che stai offrendo due modi per ottenere lo stesso risultato: "manuale" e "dolce" (con lo zucchero della sintassi). E può avere un impatto negativo sulla coerenza del codice. Offrendo 2 modi diversi per ottenere la stessa cosa qui, lì e in un altro posto si ottengono già 8 possibili combinazioni :)

void foo() {
    I1 something = new A();
    something = new B();
    something.SomeI1Function();
    something.SomeI2Function(); // we can't do this without casting first
}

OK, non puoi. Ma forse è in realtà un buon segno?

Se I1e I2implicare responsabilità diverse (altrimenti non sarebbe necessario separarle in primo luogo), allora forse foo()è sbagliato provare a far somethingeseguire due diverse responsabilità in una volta sola?

In altre parole, potrebbe essere esso foo()stesso a violare il principio della responsabilità singola e il fatto che richieda controlli di tipo casting e runtime per eseguirlo è una bandiera rossa che ci allarma.

MODIFICARE:

Se hai insistito per assicurarti che fooprendesse qualcosa che implementasse I1e I2, potresti passarli come parametri separati:

void foo(I1 i1, I2 i2) 

E potrebbero essere lo stesso oggetto:

foo(something, something);

Cosa fooimporta se sono la stessa istanza o no?

Ma se importa (ad es. Perché non sono apolidi), o pensi semplicemente che sembri brutto, potresti invece usare i generici:

void Foo<T>(T something) where T: I1, I2

Ora i vincoli generici si occupano dell'effetto desiderato senza inquinare il mondo esterno con nulla. Questo è C # idiomatico, basato su controlli in fase di compilazione, e nel complesso sembra più giusto.

Vedo in I3 : I2, I1qualche modo uno sforzo per emulare i tipi di unione in un linguaggio che non lo supporta immediatamente (C # o Java no, mentre i tipi di unione esistono in linguaggi funzionali).

Cercare di emulare un costrutto che non è realmente supportato dalla lingua è spesso ingombrante. Allo stesso modo, in C # è possibile utilizzare metodi di estensione e interfacce se si desidera emulare i mixin . Possibile? Sì. Buona idea? Non ne sono sicuro.


4

Se ci fosse un'importanza particolare nell'avere una classe implementare entrambe le interfacce, allora non vedo nulla di male nel creare una terza interfaccia che eredita da entrambe. Tuttavia, starei attento a non cadere nella trappola di cose ingegneristiche. Una classe potrebbe ereditare dall'una o dall'altra o da entrambi. A meno che il mio programma non abbia avuto molte classi in questi tre casi, è un po 'troppo creare un'interfaccia per entrambi, e certamente non se quelle due interfacce non avessero relazione l'una con l'altra (nessuna interfaccia IComparableAsSerializable per favore).

Ti ricordo che puoi creare anche una classe astratta che eredita da entrambe le interfacce e puoi usare quella classe astratta al posto di I3. Penso che sarebbe sbagliato dire che non dovresti farlo, ma dovresti almeno avere una buona ragione.


Eh? Puoi certamente implementare due interfacce in una singola classe. Non erediti le interfacce, le implementi.
Andy,

@Andy Dove ho detto che una singola classe non può implementare due interfacce? Per quanto riguarda l'uso della parola "ereditare" al posto di "attuare", semantica. In C ++, l'ereditarietà funzionerebbe perfettamente poiché le cose più vicine alle interfacce sono le classi astratte.
Neil,

L'ereditarietà e l'implementazione dell'interfaccia non sono gli stessi concetti. Le classi che ereditano più sottoclassi possono portare ad alcuni strani problemi, il che non è il caso dell'implementazione di più interfacce. E C ++ privo di interfacce non significa che la differenza scompaia, significa che C ++ è limitato in ciò che può fare. L'ereditarietà è una relazione "is-a", mentre le interfacce specificano "can-do".
Andy,

@Andy Strani problemi causati da collisioni tra metodi implementati in dette classi, sì. Tuttavia, questa non è più un'interfaccia non in nome né in pratica. Le classi astratte che dichiarano ma non implementano metodi sono in tutti i sensi della parola tranne nome, un'interfaccia. La terminologia cambia tra le lingue, ma ciò non significa che un riferimento e un puntatore non siano lo stesso concetto. È un punto pedante, ma se insisti su questo, dovremo concordare di non essere d'accordo.
Neil,

"La terminologia cambia tra le lingue" No, non lo è. La terminologia OO è coerente tra le lingue; Non ho mai ascoltato una lezione astratta che significasse qualcos'altro in ogni lingua OO che ho usato. Sì, puoi avere la semantica di un'interfaccia in C ++, ma il punto è che non c'è nulla che impedisce a qualcuno di andare e aggiungere comportamento a una classe astratta in quel linguaggio e infrangere quella semantica.
Andy,

2

Le interfacce vuote sono generalmente considerate cattive pratiche

Non sono d'accordo. Leggi le interfacce dei marker .

Mentre un'interfaccia tipica specifica funzionalità (sotto forma di dichiarazioni di metodi) che una classe di implementazione deve supportare, un'interfaccia marker non deve farlo. La semplice presenza di tale interfaccia indica un comportamento specifico da parte della classe di implementazione.


Immagino che l'OP ne sia consapevole, ma in realtà sono un po 'controversi. Mentre alcuni autori li raccomandano (ad es. Josh Bloch in "Effective Java"), alcuni sostengono che siano un antipasto e un'eredità dei tempi in cui Java non aveva ancora annotazioni. (Le annotazioni in Java sono all'incirca l'equivalente degli attributi in .NET). La mia impressione è che siano molto più popolari nel mondo Java rispetto a .NET.
Konrad Morawski,

1
Li uso una volta ogni tanto, ma sembra un po 'hacker - i lati negativi, sopra la mia testa, sono che non puoi aiutare il fatto che sono ereditati, quindi una volta che contrassegni una classe con una, tutti i suoi figli ottengono segnato anche se lo vuoi o no. E sembrano incoraggiare le cattive pratiche di utilizzo al instanceofposto del polimorfismo
Konrad Morawski,
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.