Perché è brutto avere un meccanismo di ereditarietà e sottotipizzazione?

4

È abbastanza noto che l'ereditarietà (classe) e la sottotipizzazione (a volte chiamata ereditarietà dell'interfaccia) sono cose diverse: l'ereditarietà è un meccanismo per la condivisione del codice, mentre la sottotipizzazione è una relazione che consente di sostituire un oggetto di un sottotipo dove è richiesto un oggetto di un supertipo, che è un tipo di polimorfismo.

Ho letto in diversi punti che avere un meccanismo del linguaggio di programmazione che combina entrambi è una cosa negativa. Tuttavia, molti linguaggi OO fanno proprio questo (Java è il primo esempio). Posso vedere qualche giustificazione per questo: dopotutto, se abbiamo la relazione sottotipica "B sottotipo di A", allora possiamo aspettarci che un oggetto di tipo B condivida qualche comportamento con oggetti di tipo A, quindi perché non uccidere due uccelli con una pietra e lascia che B erediti il comportamento da A?

Quindi perché questa è una brutta cosa? Sono interessato agli esempi e a una giustificazione più generale / teorica.

    
posta badweathercoder 18.04.2015 - 20:32
fonte

2 risposte

5

Non ho mai sentito questa affermazione esatta prima, ma dal modo in cui la descrivi, sembra molto strettamente correlata a due mantra con cui ho familiarità: "favorisci la composizione sull'ereditarietà" e "l'interfaccia separata dall'implementazione".

La definizione di sottotipia che sembra utilizzare (non entrerò nella questione se sia corretta o meno) è che, se A è un sottotipo di B, ciò significa che A ha la stessa interfaccia pubblica di B --possibilmente con alcune cose extra - e quell'interfaccia adempie allo stesso contratto per A come fa per B. Cerca "Principio di sostituzione di Liskov" per ulteriori informazioni.

Questo è molto diverso dal dire che A condivide parte del codice di B. Esistono molti modi per consentire alle classi di condividere il codice degli altri e (nella maggior parte dei linguaggi OOP) uno è il più semplice e sicuro per A per avere una variabile membro privata di tipo B. In altre parole, composizione.

Il problema con una classe concreta A che eredita da un'altra classe concreta B è che, oltre a dire al compilatore che A condivide l'interfaccia di B, stai forzando A a usare anche il codice di B, e non c'è un modo semplice per separare queste cose se ritieni che A debba utilizzare un'implementazione diversa da B in futuro. Quindi, diremmo che A e B sono strettamente accoppiati . Si dice spesso che l'ereditarietà è una delle forme più stringenti di accoppiamento che esiste nei moderni linguaggi di programmazione.

Se invece crei una classe astratta / un'interfaccia / qualunque-la-tua-lingua-chiama-it e fai ereditare A e B da quella, allora il compilatore capisce di avere la stessa interfaccia ma le loro implementazioni rimangono disaccoppiate per sempre. Potresti ancora avere A usare un'istanza di B internamente, ma sei libero di cambiarla in qualsiasi momento senza violare alcun codice cliente. Puoi anche cambiarlo in modo che B sia implementato usando una A se vuoi.

Ora, personalmente non direi che è un errore per i linguaggi di programmazione utilizzare lo stesso meccanismo di ereditarietà per "ereditarietà dell'interfaccia" e "ereditarietà dell'implementazione", principalmente perché nessuno di essi ti costringe a accoppiare strettamente le tue lezioni usando quel meccanismo. È possibile ereditare da classi astratte se si desidera solo "l'ereditarietà dell'interfaccia". È possibile utilizzare la composizione se si desidera solo "l'ereditarietà dell'implementazione". Inoltre, meno funzioni / meccanismi hanno un linguaggio di programmazione, più è facile imparare e utilizzare correttamente. Ma io presumo che chiunque sostenga che sia un errore sta prendendo gli argomenti che ho riassunto sopra e semplicemente facendo un passo avanti con loro.

    
risposta data 18.04.2015 - 21:13
fonte
2

I have read in several places that having one programming language mechanism that conflates both is a bad thing. [...] So why is this a bad thing?

Perché in situazioni in cui uno vuole uno di loro e non l'altro, uno è lasciato senza un'opzione (una rifusione del "voleva una banana, ma quello che hai ottenuto era un gorilla che tiene il problema della banana e dell'intera giungla", importare una citazione famosa ). "sottoclasse" come presentato in molte lingue è un'operazione che "significa molto", che ha "troppe operazioni" all'interno, con un requisito "tutto o niente".

Esempio 1 : Supponiamo che un tipo A sia introdotto da una dichiarazione di una classe "grassa" A - una classe le cui istanze richiedono una grande quantità di memoria.

Ora supponiamo che in questo sistema sia richiesto un nuovo sottotipo di A (chiamalo B ), che "estende" A con operazioni extra utilizzate in una nuova area del sistema, ma i cui valori dovrebbero essere anche utilizzato in aree vecchie (dove sono previsti valori di tipo A ). Il requisito finale per B è che anche se i valori di questo tipo si conformano rigorosamente al "contratto" di A , questi valori non hanno bisogno di - o anche non devono - occupare molta memoria.

In una lingua in cui entrambe le operazioni sono confuse in un'operazione "sottoclasse", non c'è alcuna opzione: avere un sottotipo di A utilizzato dove A è previsto, uno deve sottoclasse A e sottoclasse A , uno porta alla sottoclasse un gruppo di cose che non sono solo irrilevanti per la sottoclasse, ma esplicitamente indesiderate. Volevi solo il tipo, ma hai anche la struttura.

Esempio n. 2 : c'è anche il contrario. Cioè, una classe A esiste in un sistema, e si vuole creare una nuova struttura B basata su A ma la cui semantica, per qualche motivo, partirà dal "contratto" di A in alcuni modo ragionevole A causa di tale violazione, per motivi di sicurezza (o anche di semantica) ci si aspetta che i valori di tipo B non debbano essere usati dove sono previsti valori di tipo A (e viceversa). Di nuovo, con solo un'operazione generica di "sottoclasse", questa opzione non è disponibile. Vuoi solo la struttura, ma otterrai anche il tipo.

Mentre questo è comunemente usato attorno a un valore incapsulato di tipo A in B (altrimenti noto come "composizione"), uno è costretto a scrivere tutto il codice di colla artificiale che altrimenti non sarebbe necessario.

    
risposta data 19.04.2015 - 08:06
fonte

Leggi altre domande sui tag