Perché tornare indietro o assegnare a un supertipo piuttosto che al tipo di implementazione?

3

Ho letto molto sul polimorfismo, l'ereditarietà e la digitazione (in particolare su come si applica a Java).

Ho visto alcuni esempi interessanti, ma non molte spiegazioni sul perché.

cioè:.

    Person p = new Student();

Sto assumendo che abbiamo una classe Person e una classe Student che estende la classe Person.

La mia domanda è: perché vorresti fare questo tipo di incarico?

    
posta lext01 05.10.2014 - 05:30
fonte

3 risposte

1

Usando qualcosa del genere, possiamo avere molti tipi diversi che supportano l'interfaccia di Person , il che significa che possiamo scrivere codice che prende un Person e non interessa quale specifico digita, purché supporti qualsiasi supporto di Person .

Potresti non anticipare una classe AncientZombieLord quando scrivi un codice generico che prende un Person , ma se AncientZombieLord è un sottotipo di Person , tutto il codice scritto per Person funzionerà per AncientZombieLord anche.

Se dai un'occhiata alle raccolte di Java, ci sono ArrayList e LinkedList types, che sono entrambi sottotipi di List . Hanno caratteristiche di performance diverse, ma entrambi supportano un'interfaccia comune, quindi posso scrivere codice che usa un List che funzionerà con entrambi i tipi di lista.

Puoi usare questo tipo di cose per scrivere un algoritmo generico che usa un tipo ora, e poi passarlo a qualcos'altro senza dover trafficare con un sacco di codice - basta cambiare un tipo.

In generale, essere in grado di astrarre dai dettagli che non contano è una grande vittoria.

    
risposta data 05.10.2014 - 06:03
fonte
8

Gli esempi inventati tradizionalmente forniti in tali problemi non catturano correttamente il ragionamento del perché il polimorfismo è buono e utile.

Consente invece di guardare esempi del mondo reale che si trovano nel framework Collections di Java. Cose che implementano Elenco . Delle "Tutte le classi di implementazione conosciute", ce ne sono due che sono più interessanti per questo esempio: LinkedList e ArrayList .

LinkedList è supportato da un elenco collegato e ArrayList è supportato da un array. Queste strutture hanno diversi vantaggi e svantaggi nel fare determinate operazioni. Un ArrayList è molto veloce per trovare l'elemento arbitrario n th , mentre LinkedList è lento per questo. D'altra parte, attaccare un elemento all'inizio o alla fine di una LinkedList è sempre abbastanza veloce, mentre l'ArrayList che mette un elemento in primo piano è doloroso lento e la fine può essere anche piuttosto brutta se ha bisogno di far crescere la matrice.

Quindi, c'è una ragione per cui sceglierei un'implementazione o l'altra. Ora, se restituisco un valore da una funzione ... ArrayList<Integer> foo() significherebbe che non potrei mai cambiare l'implementazione dietro di esso. Se in seguito deciderò di volere un LinkedList , non potrei farlo perché farebbe sì che tutto il codice che lo utilizza si interrompa e debba essere modificato in LinkedList<Integer> .

Invece, posso dire "non importa quale lista ti offro - non è necessario sapere come la sto implementando". E invece ti do solo un metodo List<Integer> foo() . Hai bisogno di sapere se è un LinkedList? o ArrayList? No. Se tutto ciò che mi interessa è restituirti una lista (una raccolta di elementi in un ordine che potrebbe contenere duplicati), questo è quello che stai ricevendo. Rende più facile per me e te sapere meno dell'implementazione.

Questo fa parte del principio di minima conoscenza che è un componente della legge di Demeter. Se non dovessi saperlo, non dovrei parlartene. Ti impedisce di raggiungere attraverso l'interfaccia l'implementazione sottostante e di assumere le caratteristiche su come funziona.

Nel tuo esempio, hai una Persona e quell'interfaccia (o classe astratta) è tutto ciò di cui hai bisogno per fare ciò che devi fare. Permette un accoppiamento più morbido e una maggiore flessibilità nel design in seguito, e questa è una buona cosa.

Esistono strumenti di analisi statica che possono aiutarti a scrivere codice più in linea con queste linee guida. Uno di questi esempi è pmd . Nelle sue regole di tipo , esiste una regola di coping che fornisce avvisi come: "Evita di usare tipi di implementazione (es. HashSet); usa invece l'interfaccia (es. Set) "- e questo è un buon consiglio. (Questione SO relativa all'avviso: Perché l'interfaccia per una classe Java deve essere preferita? )

    
risposta data 05.10.2014 - 05:47
fonte
0

@ La risposta di MichaelT è eccellente dal punto di vista teorico, ma ecco una spiegazione più concreta:

In questo caso d'uso, che è solo un esempio di digitazione dinamica, il vantaggio non è ovvio guardando solo la singola riga di codice nell'esempio. Supponiamo che Person sia un'interfaccia, una classe astratta o anche una classe base concreta in cui è esposto un metodo talk() . Dichiarando un'istanza Student come Person , le funzioni possono accettare un tipo generico (cioè Person ), sapendo che quando il codice viene eseguito , sarà%% di% di% metodo diStudent che viene eseguito, non quello che potrebbe essere in talk() (se definisce anche un corpo di un metodo).

abstract class Person {
  void talk() {
    System.out.println("I am a person."); // default behavior
  }
}

class Student extends Person {
  @Override
  void talk() {
    System.out.println("I am a student.");
  }
}

// Elsewhere...
public static void main(String[] args) {
    Person p = new Student();
    p.talk();
}

// Output:
I am a student.

In questo modo, è possibile avere più implementazioni dello stesso metodo concettuale, senza dover ricorrere alle opzioni Person o se / else alla logica.

Per una comprensione più completa della digitazione dinamica, esamina come funziona un linguaggio tipizzato completamente dinamicamente come JavaScript o guarda la parola chiave instanceof di C # e l'utilizzo.

    
risposta data 05.10.2014 - 08:20
fonte

Leggi altre domande sui tag