Il polimorfismo del sottotipo distingue tra comportamento ereditario o ereditare un'interfaccia?

1

Per fornire un contesto, ho visto recentemente alcuni commenti che equivalgono ad ereditare il comportamento da un supertipo, ereditando un'interfaccia pura senza alcun comportamento. Ma ci sono conseguenze piuttosto significative e diverse per ciascuno.

es. Ecco una classe base di esempio e un'interfaccia "pura" (pseudocodice):

class FlyingBird {
    void fly() {
        change state due to flight
    }
}

interface Flyable {
    void fly();
}

Ora diciamo che ho Swan and Dove. Sarò sottoclasse Swan da FlyingBird, e farò implementare la colomba Flyable.

Swan extends FlyingBird {
    (no defnition for fly, because we inherit it from FlyingBird)
}

Dove implements Flyable {
    void fly() {
        ... must provide implementation for fly, because the interface only provides the signature
    }

Ora posso chiamare sia swan.fly () che dove.fly (), ma swan ottiene la sua implementazione da FlyingBird. L'espressione "Favorisci la composizione sull'ereditarietà" mi dice di favorire l'implementazione di Colomba su Swan, con una sorta di implementazione iniettata, ma "il polimorfismo del sottotipo" non sembra fare questa distinzione.

La distinzione è importante, perché, ad esempio:

  • C'è un crescente movimento in OO per rendere le classi "definitive" o "non-estendibili" di default, a causa della complessità del comportamento ereditario senza rompere la semantica delle super-classi. In altre parole, alcune persone suggeriscono che una forma di prototipo di sottotipo debba essere disabilitata di default. (Non ho sentito nessuno suggerire di eliminare le interfacce in Java.)

  • "Favorisci la composizione sull'ereditarietà" differenzia il comportamento di condivisione attraverso l'ereditarietà della classe e fornisce un'interfaccia comune supportata da un'implementazione condivisa.

In altre parole, ci sono differenze pratiche e importanti tra il comportamento ereditario e l'ereditare un'interfaccia.

Il polimorfismo del sottotipo distingue tra comportamento ereditario o ereditare un'interfaccia?

    
posta Rob 09.04.2014 - 19:31
fonte

2 risposte

2

Now, I can call both swan.fly() and dove.fly(), but swan gets its implementation from FlyingBird. The idiom "Favor composition over inheritance" tells me to favor the Dove implementation over Swan, but "subtype polymorphism" doesn't seem to make that distinction.

Does subtype polymorphism distinguish between inheriting behavior, or inheriting an interface?

Non è così. I criteri per il polimorfismo sono la sostituibilità del comportamento. È possibile avere il polimorfismo del sottotipo senza classi, l'ereditarietà dell'implementazione o le interfacce coinvolte.

Ad esempio, prendi in considerazione le tuple - una sequenza immutabile di valori di n di tipi possibilmente diversi. Per esempio. il tipo (float, float) è il tipo di 2 tuple il cui primo elemento è un numero intero e il cui secondo elemento è un numero in virgola mobile. L'unica operazione che i tipi di tupla hanno è la selezione: il recupero dell'elemento i th della tupla (per i <= n ). Quindi, l'unica cosa che possiamo fare con un valore (float, float) è recuperare il primo elemento o il secondo elemento (il float ).

Ora considera il tipo (float, float, string) . Questo è un tipo distinto da (float, float) , ma possiamo applicare le stesse operazioni su di esso: possiamo recuperare il primo elemento e i secondi elementi ei tipi corrispondono. Quindi potremmo sostituire un (float, float, string) ovunque sia previsto un (float, float) e il programma funzionerebbe comunque.

Il sistema di tipo linguistico può o non può decidere di consentire tali sostituzioni. Da un lato, consentendo loro di aumentare il numero di programmi validi nella lingua. D'altra parte, stai indebolendo le condizioni che puoi affermare attraverso il sistema di tipi - ora non puoi imporlo che se una funzione accetta un (float, float) come argomento, in realtà riceve un (float, float) e non alcuni 3- tupla (o 4-tupla o ...) che può essere sostituibile. Ad esempio, potresti passare inavvertitamente un (float, float, float) che rappresenta i dati dei pixel Rosso / Blu / Verde a una funzione che prevede una (float, float) che rappresenta un punto 2D e il programma verrà compilato.

Si noti, tuttavia, che la lingua non può forzare la sostituzione dei tipi definiti dall'utente. Niente ti impedisce di creare un Penguin implements Flyable che genera semplicemente un'eccezione quando chiami fly() . L'onere di garantire la sostituibilità ricade sul programmatore. E se tu deliberatamente classi di progettazione che rompono i contratti della loro superclasse, infliggi molto dolore e infelicità a te stesso e agli altri. Ma questo non è niente di nuovo - il sistema di tipi può solo garantire che se un'espressione restituisce un valore, è del tipo corretto. Solo perché scrivo una funzione double squareRoot(double x) , non significa automaticamente che restituirò effettivamente la radice quadrata di x . Dovrei, ma il compilatore non può forzarmi.

Per quanto riguarda il motivo per cui dovresti favorire la composizione sull'ereditarietà, questa è una domanda diversa .

    
risposta data 09.04.2014 - 20:21
fonte
0

Nel tuo esempio, Flyable e FlyingBird non sono correlati. Pertanto non c'è una vera distinzione da fare per il compilatore , vede solo due oggetti completamente diversi che hanno ciascuno un metodo fly() .

Se si dichiara una variabile Flyable e si chiama il suo metodo fly , indipendentemente dall'istanza, sarà il metodo di istanza che corrisponde al metodo dell'interfaccia che verrà chiamato.

Se, tuttavia, dichiari una variabile FlyingBird e ne incidi un'istanza Swan , poiché FlyingBird non sa nulla di Flyable , quindi è il metodo base FlyingBird.fly() che verrà eseguito.

Ciò che potrebbe essere confuso è se il tuo FlyingBird stava implementando Flyable ma avrebbe comunque il proprio metodo fly . Logicamente avresti la tua implementazione Flyable.fly() sul metodo FlyingBird.fly() , quindi di nuovo non ci sarebbe alcuna distinzione da fare. Tuttavia, alcune lingue come consentono di nascondere i metodi di interfaccia dietro i metodi di istanza denominati in modo completamente diverso, potrebbe persino essere privato. Pertanto, se una variabile bird era di tipo Flyable e l'implementazione dell'istanza avrebbe rinominato il metodo land() , è il metodo che verrebbe chiamato. Per quanto poco sensato, è perfettamente legale per la tua classe determinare che le chiamate Flyable.fly() devono essere associate al metodo land() .

Se tuttavia la variabile era di tipo FlyingBird , chiamare bird.fly() chiamerà quel metodo esatto indipendentemente dall'interfaccia Flyable .

Ovviamente questo è marginale perché normalmente non faresti mai qualcosa del genere. Tuttavia, mostra come il compilatore fa la distinzione. Una variabile tipizzata dall'interfaccia chiamerà il metodo dell'interfaccia, mentre una variabile tipizzata dalla classe chiamerà il metodo come definito nella classe.

    
risposta data 09.04.2014 - 19:59
fonte

Leggi altre domande sui tag