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

12

Le interfacce vuote sono generalmente considerate una cattiva pratica, per quanto ne so, specialmente dove cose come gli attributi sono supportate dalla lingua.

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

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

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

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

// 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
    }
}
    
posta Kai 21.07.2015 - 17:10
fonte

3 risposte

27

Anything that implements I3 will need to implement I1 and I2, and objects from different classes that inherit I3 can then be used interchangeably (see below), so is it right to call I3 empty? If so what would be a better way to architecture this?

"Raggruppare" I1 e I2 in I3 offre una bella scorciatoia (?), o una sorta di sintassi per lo zucchero dove vuoi che siano entrambi implementati.

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 - mentre : I3 è equivalente a : I1, I2 , : I1, I2 non è un equivalente di : I3 . Anche se sono funzionalmente identici, una classe che supporta sia I1 che I2 non verrà rilevata come supporto I3 .

Questo significa che stai offrendo due modi per realizzare la stessa cosa: "manuale" e "dolce" (con lo zucchero della sintassi). E può avere un impatto negativo sulla coerenza del codice. Offrire 2 diversi modi per realizzare la stessa cosa qui, lì, e in un altro luogo risulta già in 8 combinazioni possibili:)

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 è davvero un buon segno?

Se I1 e I2 implicano responsabilità diverse (altrimenti non sarebbe necessario separarli in primo luogo), allora forse foo() è sbagliato per provare a fare something eseguire due diverse responsabilità in una partire?

In altre parole, potrebbe essere che foo() in sé viola il principio di Single Responsibility, e il fatto che richieda il casting e i controlli del tipo di esecuzione per rimuoverlo è una bandiera rossa che ci allarma.

Modifica

Se hai insistito per garantire che foo richieda qualcosa che implementa I1 e I2 , potresti passarli come parametri separati:

void foo(I1 i1, I2 i2) 

E potrebbero essere lo stesso oggetto:

foo(something, something);

Che cosa importa foo se sono la stessa istanza o no?

Ma se si preoccupa (ad esempio perché non sono apolidi), o pensi solo che questo sembra brutto, si potrebbe usare invece generici:

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

Ora i vincoli generici si prendono cura dell'effetto desiderato mentre non inquinano il mondo esterno con nulla. Questo è C # idiomatico, basato su verifiche in fase di compilazione, e sembra più giusto nel complesso.

Vedo I3 : I2, I1 in qualche modo come uno sforzo per emulare tipi di unione in una lingua che non lo supporta della scatola (C # o Java no, mentre i tipi di unione esistono nei linguaggi funzionali).

Cercare di emulare un costrutto che non è realmente supportato dalla lingua è spesso clunky. Analogamente, in C # puoi utilizzare i metodi di estensione e le interfacce se desideri emulare mixins . Possibile? Sì. Buona idea? Non sono sicuro.

    
risposta data 21.07.2015 - 17:28
fonte
4

Se ci fosse una particolare importanza nell'avere una classe implementare entrambe le interfacce, allora non vedo nulla di sbagliato nella creazione di una terza interfaccia che eredita da entrambe. Tuttavia, starei attenta a non cadere nella trappola delle cose di overengineering. Una classe potrebbe ereditare da una o dall'altra o entrambe. A meno che il mio programma non abbia molte classi in questi tre casi, è un po 'troppo creare un'interfaccia per entrambi, e certamente non se queste due interfacce non hanno alcuna relazione tra loro (nessuna interfaccia IComparableAndSerializable 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 per lo meno dovresti avere una buona ragione.

    
risposta data 21.07.2015 - 17:22
fonte
2

Empty interfaces are generally consider bad practice

Non sono d'accordo. Leggi le interfacce degli indicatori .

Whereas a typical interface specifies functionality (in the form of method declarations) that an implementing class must support, a marker interface need not do so. The mere presence of such an interface indicates specific behavior on the part of the implementing class.

    
risposta data 22.07.2015 - 00:23
fonte

Leggi altre domande sui tag