2015-12-26 8 views
24

Le interfacce sono eccezionali dal punto di vista della flessibilità. Ma nel caso in cui un'interfaccia è utilizzata da un gran numero di client. L'aggiunta di nuovi metodi all'interfaccia mantenendo intatti i vecchi mehtod interromperà il codice di tutti i client poiché nuovi metodi non saranno presenti nei client. Come mostrato di seguito:Le interfacce possono evolversi con il tempo?

public interface CustomInterface { 

    public void method1(); 

} 

public class CustomImplementation implements CustomInterface { 

    @Override 
    public void method1() { 
     System.out.println("This is method1"); 
    } 
} 

Se ad un certo punto più avanti nel tempo, aggiungiamo un altro metodo a questa interfaccia codice di tutti i clienti si romperà.

public interface CustomInterface { 

    public void method1(); 

    public void method2(); 

} 

Per evitare questo, dobbiamo implementare in modo esplicito nuovi metodi nel codice di tutti i client.

Quindi penso di interfacce e questo scenario come segue:

  1. Interfacce volta scritti sono come la scultura in pietra. Si suppone raramente e si prevede che cambi. E se lo fanno, hanno un costo enorme (riscrivendo l'intero codice) che i programmatori dovrebbero essere pronti.
  2. In seguito al punto precedente, è possibile scrivere interfacce in grado di resistere alla prova del tempo?
  3. Come viene gestito tale scenario nelle interfacce in cui prevedi funzionalità aggiuntive in futuro? Ciò sta anticipando il cambiamento nel contratto con cui tutti i clienti sono vincolati.

MODIFICA: il metodo Default è davvero una bella aggiunta alle interfacce Java che molte persone hanno menzionato nelle loro risposte. Ma la mia domanda era più nel contesto della progettazione del codice. E come forzare l'implementazione del metodo sul client è un carattere intrinseco di un'interfaccia. Ma questo contratto tra un'interfaccia e un client sembra fragile mentre la funzionalità si evolverà alla fine.

+0

Suggerimento di lettura: le linee guida di progettazione del framework (è .net ma è anche una buona lettura per il programmatore java) ha una sezione sul chosing tra classe o interfaccia. Come affermato da altre risposte, anche loro parlano dell'utilizzo delle classi poiché sono molto più flessibili delle interfacce e consentono di evolvere senza infrangere il codice client. –

risposta

21

Una soluzione a questo problema è stata introdotta in Java 8 sotto forma di metodi predefiniti nelle interfacce. Ha permesso di aggiungere nuovi metodi alle interfacce Java SE esistenti senza rompere il codice esistente, poiché ha fornito l'implementazione predefinita a tutti i nuovi metodi.

Ad esempio, l'interfaccia Iterable, che è ampiamente utilizzato (si tratta di un super un'interfaccia dell'interfaccia Collection) è stato aggiunto due nuovi metodi predefiniti - default void forEach(Consumer<? super T> action) e default Spliterator<T> spliterator().

+0

ok, vorrei esaminarlo. Ma prima di Java 8 dal punto di vista del design, le interfacce dovrebbero essere immutabili? Questo è il momento in cui è stato stipulato un contratto con i clienti che non dovrebbe cambiare. –

18
public interface CustomInterface { 
    public void method1(); 
} 

public interface CustomInterface2 extends CustomInterface { 
    public void meathod2(); 
} 

Altro che metodo predefinito è possibile utilizzare l'ereditarietà proprietà come esposizione qui sopra con la quale la nuova interfaccia avrà tutto il metodo precedente con nuovi metodi e utilizzare questa interfaccia nella vostra situazione richiesto.

+1

Ma non è quello che sto cercando. Perché con il codice sopra, il codice client si interromperà ancora. –

+2

e come si rompe ??? –

+1

poiché il metodo 2 non è ancora implementato nel codice client. –

4

Le interfacce sono contratti tra lo sviluppatore e i clienti, quindi hai ragione: sono scolpiti nella pietra e non devono essere modificati. Pertanto, un'interfaccia dovrebbe esporre (= richiesta) solo la funzionalità di base che è assolutamente richiesta da una classe.

Prendere l'interfaccia List ad esempio. Ci sono molte implementazioni di liste in Java, molte delle quali si evolvono nel tempo (migliori algoritmi sotto il cofano, migliorata memoria di archiviazione), ma il "concetto" di base di un elenco - aggiungi un oggetto, cerca un oggetto, rimuovi un articolo - non dovrebbe e non cambierà mai.

Quindi, per la tua domanda: invece di scrivere le interfacce che le classi implementano, puoi usare classi astratte. Le interfacce sono fondamentalmente classi puramente astratte, nel senso che non forniscono alcuna funzionalità integrata.Tuttavia, uno può aggiungere nuovi metodi non astratti a una classe astratta che i client non dovranno implementare (override).

prendere questa classe astratta (= interfaccia) per esempio:

abstract class BaseQueue { 
    abstract public Object pop(); 
    abstract public void push(Object o); 
    abstract public int length(); 
    public void clearEven() {}; 
} 

public class MyQueue extends BaseQueue { 
    @Override 
    public Object pop() { ... } 

    ... 
} 

Proprio come nelle interfacce, ogni classe che estende BaseQueue è contrattualmente tenuto a implementare i metodi astratti. Il metodo clearEven(), tuttavia, non è un metodo astratto (e già viene fornito con un'implementazione vuota), quindi il client non è obbligato a implementarlo o addirittura a utilizzarlo.

Ciò significa che è possibile sfruttare la potenza delle classi astratte in Java per creare metodi di associazione non contrattuali. Puoi aggiungere altri metodi alla classe base in futuro quanto vuoi, purché non siano metodi astratti.

+1

Questo è equivalente all'utilizzo di 'default', tranne l'utilizzo di classi astratte invece di interfacce introduce un intero host di mal di testa a causa di Java che vieta l'ereditarietà multipla. Ma ho downvoted questo perché l'implementazione vuota di clearEven() in realtà viola il contratto effettivo del metodo, in particolare che "cancella anche" (qualunque cosa sia). Un modo migliore sarebbe quello di lanciare UnsupportedOperationException, ma sarebbe comunque una cattiva pratica. BaseQueue <- L'ereditarietà di ClearEvenableQueue è la soluzione pulita in una mentalità OO. –

+0

@ SáT Limitare l'ereditarietà a un singolo tipo è un principio di progettazione molto razionale e utile nella programmazione OO.Quindi tendo ad essere in disaccordo sul fatto che le classi astratte sono un mal di testa da implementare solo perché non supportano l'ereditarietà multipla. Stai confrontando le interfacce e le classi astratte, ed è per questo che trovi le classi astratte meno flessibili delle interfacce. Ma entrambi hanno uno scopo molto diverso nell'architettura del codice e non dovrebbero essere confrontati. Anche se sono d'accordo con il tuo secondo e terzo punto. –

8

Java 8 ha introdotto l'implementazione predefinita per i metodi. Queste implementazioni risiedono nell'interfaccia. Se un nuovo metodo con un'implementazione predefinita viene creato in un'interfaccia già implementata da molte classi, non è necessario modificare tutte le classi, ma solo quelle che vogliamo avere un'implementazione diversa per il metodo appena definito rispetto alla uno di default.

Ora, che dire delle versioni precedenti di Java? Qui possiamo avere un'altra interfaccia che estende la prima. Successivamente, le classi che vogliamo implementare il metodo appena dichiarato verranno modificate per implementare la nuova interfaccia. Come mostrato di seguito.

public interface IFirst { 
    void method1(); 
} 

public class ClassOne implements IFirst() { 
    public void method1(); 
} 

public class ClassTwo implements IFirst() { 
    public void method1(); 
} 

Ora, vogliamo method2() dichiarato, ma dovrebbe essere attuata solo da ClassOne.

public interface ISecond extends iFirst { 
    void method2(); 
} 

public class ClassOne implements ISecond() { 
    public void method1(); 
    public void method2(); 
} 

public class ClassTwo implements IFirst() { 
    public void method1(); 
} 

Questo approccio sarà ok nella maggior parte dei casi, ma ha anche aspetti negativi. Ad esempio, vogliamo method3() (e solo quello) per ClassTwo. Avremo bisogno di una nuova interfaccia IThird. Se in un secondo momento dovremo aggiungere method4() che dovrebbe essere implementato sia da ClassOne che da ClassTwo, sarà necessario modificare (ma non implemented IFirst) e .

Raramente c'è un "mirino magico" quando si parla di programmazione. Nel caso di interfacce, è meglio se non cambiano. Questo non è sempre il caso in situazioni di vita reale. Ecco perché è consigliabile che le interfacce offrano solo "il contratto" (funzionalità indispensabile) e, quando possibile, utilizzare classi astratte.

5

Un cambio di interfaccia futuro non dovrebbe interrompere tutto ciò che è stato funzionante - se lo fa, è un'interfaccia diversa. (Può deprecare le cose, tuttavia, e un ciclo completo dopo la deprecazione può essere accettabile per dire che lanciare un'eccezione Unimplemented è accettabile.)

Per aggiungere cose a un'interfaccia, la risposta più pulita è derivare una nuova interfaccia da esso. Ciò consentirà di utilizzare gli oggetti che implementano i nuovi comportamenti con il codice che si aspetta quelli vecchi, lasciando che l'utente dichiari in modo appropriato e/o typecast per ottenere l'accesso alle nuove funzionalità. È un po 'fastidioso poiché potrebbe richiedere l'esecuzione di test, ma è l'approccio più affidabile, ed è quello che vedrai in molti standard del settore.

2

Penso che la tua domanda riguardi più la progettazione e le tecniche, quindi le risposte di java8 sono un po 'fuorvianti. Questo problema era noto molto prima di java8, quindi ci sono altre soluzioni per questo.

In primo luogo, non esistono metodi assolutamente privi di costi per risolvere un problema. Le dimensioni delle incongruenze derivanti dall'interfaccia evolutiva dipendono dal modo in cui viene utilizzata la biblioteca e dal modo in cui è deliberata la progettazione.

1) Nessun tecniche vi aiuteranno, se progettato un'interfaccia e dimenticato di includere un metodo obbligatoria in esso. Pianifica meglio il tuo design e prova ad anticipare in che modo i client utilizzeranno le tue interfacce.
Esempio: Imagine interfaccia che ha il metodo turnOn() ma manca il metodo turnOff(). L'introduzione di un nuovo metodo con l'implementazione vuota predefinita in java8 preverrà errori di compilazione, ma non sarà di grande aiuto, perché chiamare un metodo non avrà alcun effetto. Fornire implementazione lavorativa a volte è impossibile perché l'interfaccia non ha campi e stato.

2) Diverse implementazioni di solito hanno cose in comune. Non aver paura di mantenere una logica comune nella classe genitore. Eredita le tue classi di libreria da questa classe genitore. Ciò imporrà ai client della libreria di ereditare le proprie implementazioni anche dalla classe genitore. Ora puoi apportare piccole modifiche all'interfaccia senza rompere tutto.
Esempio: hai deciso di includere il metodo isTurnedOn() nell'interfaccia. Con una classe base, puoi scrivere un'implementazione del metodo di default che avrebbe reso sence. Le classi che non sono state ereditate dalla classe padre devono ancora fornire le proprie implementazioni del metodo, ma poiché il metodo non è obbligatorio, sarà facile per loro.

3) L'aggiornamento della funzionalità viene generalmente ottenuto estendendo le interfacce. Non c'è motivo di imporre ai client di libreria di implementare un sacco di nuovi metodi perché potrebbero non averne bisogno.
Esempio: hai deciso di aggiungere il metodo stayIdle() all'interfaccia. Fa la differenza per le classi nella tua libreria, ma non per le classi client personalizzate. Poiché questa funzionalità è nuova, è preferibile creare una nuova interfaccia che estenda Machine e utilizzarla quando è necessaria.