Come gestire i metodi che sono stati aggiunti per i sottotipi nel contesto del polimorfismo?


14

Quando usi il concetto di polimorfismo crei una gerarchia di classi e usando i riferimenti dei genitori chiami le funzioni dell'interfaccia senza sapere quale tipo specifico ha l'oggetto. Questo è fantastico Esempio:

Hai una collezione di animali e chiami la funzione di tutti gli animali eate non ti importa se si tratta di un cane che mangia o di un gatto. Ma nella stessa gerarchia di classi hai animali che hanno ulteriori - oltre a quelli ereditati e implementati dalla classe Animal, ad esempio makeEggs, getBackFromTheFreezedStatee così via. Quindi in alcuni casi nella tua funzione potresti voler conoscere il tipo specifico per chiamare comportamenti aggiuntivi.

Ad esempio, nel caso in cui è giunto il momento di mattina e se è solo un animale, allora si chiama eat, altrimenti se si tratta di un essere umano, quindi chiamare prima washHands, getDressede solo allora chiamare eat. Come gestire questi casi? Il polimorfismo muore. Devi scoprire il tipo di oggetto, che suona come un odore di codice. Esiste un approccio comune per gestire questi casi?


7
Il tipo di polimorfismo che hai descritto si chiama polimorfismo dei sottotipi , ma non è l'unico tipo (vedi polimorfismo ). Non è necessario creare una gerarchia di classe per eseguire il polimorfismo (e in realtà direi che l'ereditarietà non è il metodo più comune per ottenere il polimorfismo dei sottotipi, l'implementazione di un'interfaccia è molto più diffusa).
Vincent Savard,

24
Se definisci Eaterun'interfaccia con il eat()metodo, quindi come client, non ti interessa che Humanun'implementazione debba prima chiamare washHands()ed getDressed()è dettagli di implementazione di questa classe. Se, come cliente, ti interessa questo fatto, molto probabilmente non stai utilizzando lo strumento corretto per il lavoro.
Vincent Savard,

3
Devi anche considerare che, al mattino, potrebbe essere necessario un essere umano getDressedprima di loro eat, non è così per il pranzo. A seconda delle circostanze, washHands();if !dressed then getDressed();[code to actually eat]potrebbe essere il modo migliore per implementarlo per un essere umano. Un'altra possibilità è cosa succede se altre cose lo richiedono washHandse / o getDressedvengono chiamati? Supponi di avere leaveForWork? Potrebbe essere necessario strutturare il flusso del programma in modo tale che venga chiamato molto prima.
Duncan X Simpson,

1
Tieni presente che il controllo del tipo esatto può essere un odore di codice in OOP ma è una pratica molto comune in FP (cioè usa la corrispondenza dei modelli per determinare il tipo di unione discriminata e poi agire su di essa).
Theodoros Chatzigiannakis,

3
Fai attenzione agli esempi di aule scolastiche delle gerarchie OO come gli animali. I programmi reali non hanno quasi mai tassonomie così pulite. Ad esempio, ericlippert.com/2015/04/27/wizards-and-warriors-part-one . O se vuoi andare fino in fondo e mettere in discussione l'intero paradigma: la programmazione orientata agli oggetti è cattiva .
jpmc26,

Risposte:


18

Dipende. Purtroppo non esiste una soluzione generica. Pensa alle tue esigenze e prova a capire cosa dovrebbero fare queste cose.

Ad esempio, hai detto che al mattino animali diversi fanno cose diverse. Che ne dici di introdurre un metodo getUp()o di prepareForDay()o qualcosa del genere. Quindi puoi continuare con il polimorfismo e lasciare che ogni animale esegua la sua routine mattutina.

Se si desidera differenziare gli animali, non è necessario memorizzarli indiscriminatamente in un elenco.

Se nient'altro funziona, allora potresti provare il modello visitatore , che è una specie di hack per consentire un tipo di dispacciamento dinamico in cui puoi inviare un visitatore che riceverà richiamate esatte per tipo dagli animali. Sottolineo tuttavia che questo dovrebbe essere l'ultima risorsa se tutto il resto fallisce.


33

Questa è una buona domanda ed è il tipo di problema che molte persone cercano di capire come usare OO. Penso che la maggior parte degli sviluppatori abbia difficoltà con questo. Vorrei poter dire che molti lo superano, ma non sono sicuro che sia così. La maggior parte degli sviluppatori, secondo la mia esperienza, finisce per usare borse di proprietà pseudo-OO .

Innanzitutto, vorrei essere chiaro. Non è colpa tua. Il modo in cui viene insegnato OO è altamente imperfetto. L' Animalesempio è il principale colpevole, l'IMO. Fondamentalmente, diciamo, parliamo di oggetti, cosa possono fare. Una Animallattina eat()e può speak(). Super. Ora crea alcuni animali e codifica come mangiano e parlano. Ora conosci OO, giusto?

Il problema è che questo sta arrivando a OO dalla direzione sbagliata. Perché ci sono animali in questo programma e perché hanno bisogno di parlare e mangiare?

Ho difficoltà a pensare a un uso reale per un Animaltipo. Sono sicuro che esiste ma discutiamo di qualcosa su cui penso sia più facile ragionare: una simulazione del traffico. Supponiamo di voler modellare il traffico in vari scenari. Ecco alcune cose di base che dobbiamo avere per poterlo fare.

Vehicle
Road
Signal

Possiamo andare più in profondità con ogni genere di cose, pedoni e treni, ma lo faremo semplice.

Consideriamo Vehicle. Di quali capacità ha bisogno il veicolo? Deve viaggiare su una strada. Deve essere in grado di fermarsi ai segnali. Deve essere in grado di navigare negli incroci.

interface Vehicle {
  move(Road road);
  navigate(Road... intersection);
}

Questo è probabilmente troppo semplice ma è un inizio. Adesso. Che dire di tutte le altre cose che un veicolo potrebbe fare? Possono svoltare da una strada e trasformarsi in un fossato. Fa parte della simulazione? No. Non ne ho bisogno. Alcune auto e autobus hanno l'idraulica che consente loro rispettivamente di rimbalzare o inginocchiarsi. Fa parte della simulazione? No. Non ne ho bisogno. La maggior parte delle auto brucia benzina. Alcuni no. La centrale elettrica fa parte della simulazione? No. Non ne ho bisogno. Dimensioni della ruota? Non ne ho bisogno. Navigazione GPS? Sistema di infotainment? Non ne ho bisogno.

Devi solo definire i comportamenti che intendi utilizzare. A tal fine, penso che spesso sia meglio creare interfacce OO dal codice che interagisce con esse. Si inizia con un'interfaccia vuota e quindi si inizia a scrivere il codice che chiama i metodi inesistenti. Ecco come sai quali metodi hai bisogno sulla tua interfaccia. Quindi, una volta che lo hai fatto, vai e inizi a definire le classi che implementano questi comportamenti. I comportamenti che non vengono utilizzati sono irrilevanti e non devono essere definiti.

Il punto centrale di OO è che è possibile aggiungere nuove implementazioni di queste interfacce in un secondo momento senza modificare il codice chiamante. L'unico modo che funziona è se le esigenze del codice chiamante determinano ciò che va nell'interfaccia. Non c'è modo di definire tutti i comportamenti di tutte le possibili cose che potrebbero essere pensati in seguito.


13
Questa è una buona risposta "A tal fine, penso che spesso sia meglio creare interfacce OO dal codice che interagisce con esse." Assolutamente, e direi che è l'unico modo. Non puoi conoscere il contratto pubblico di un'interfaccia solo attraverso l'implementazione, è sempre definito dal punto di vista dei suoi clienti. (E come nota a margine, questo è in realtà il TDD.)
Vincent Savard,

@VincentSavard "Direi che è l'unico modo." Hai ragione. Immagino che il motivo per cui non l'ho reso così assoluto sia che una volta che hai avuto l'idea, puoi in qualche modo perfezionare l'interfaccia e quindi perfezionarla in questo modo. Alla fine, quando si arriva ai chiodi di ottone, è l'unica cosa che conta.
JimmyJames,

@ jpmc26 Forse un po 'fortemente formulato. Non sono sicuro di essere d'accordo sul fatto che sia raro implementarlo. Non sono sicuro di come le interfacce possano essere utili se non le stai usando in questo modo a parte le interfacce marker che penso siano un'idea terribile.
JimmyJames,

9

TL; DR:

Pensa a un'astrazione e ai metodi che si applicano a tutte le sottoclassi e coprono tutto ciò di cui hai bisogno.

Per prima cosa restiamo con il tuo eat()esempio.

È una proprietà dell'essere umano che, come prerequisito per mangiare, gli umani vogliono lavarsi le mani e vestirsi prima di mangiare. Se vuoi che qualcuno venga da te a fare colazione con te, non dici loro di lavarsi le mani e di vestirsi, lo fanno da soli quando li inviti o rispondono "No, non posso venire non mi sono lavato le mani e non sono ancora vestito ".

Torna al software:

Come Humanesempio non mangiare senza i presupposti, mi piacerebbe avere la Human's eat()metodo di farlo washHands()e getDressed()se questo non è stato fatto. Non dovrebbe essere il tuo lavoro di eat()chiamante conoscere questa peculiarità. L'alternativa dell'essere umano testardo sarebbe quella di gettare un'eccezione ("Non sono disposto a mangiare!") Se le condizioni preliminari non sono soddisfatte, lasciandoti frustrato, ma almeno informato che mangiare non ha funzionato.

Che dire makeEggs()?

Consiglierei di cambiare il tuo modo di pensare. Probabilmente vuoi eseguire i compiti mattutini programmati di tutti gli esseri. Ancora una volta, come chiamante non dovrebbe essere il tuo lavoro sapere quali sono i loro doveri. Quindi consiglierei un doMorningDuties()metodo implementato da tutte le classi.


Sono d'accordo con questa risposta. Narek ha ragione sull'odore del codice. È il design dell'interfaccia che è puzzolente, quindi risolvi questo e il tuo bene.
Jonathan van de Veen,

Ciò che questa risposta descrive è di solito indicato come Principio di sostituzione di Liskov .
Philipp,

2

La risposta è abbastanza semplice

Come gestire oggetti che possono fare più di quanto ti aspetti?

Non è necessario gestirlo perché non servirebbe a nulla. Un'interfaccia è in genere progettata in base al modo in cui verrà utilizzata. Se la tua interfaccia non definisce le mani di lavaggio, non ti interessa come chiamante dell'interfaccia; se lo avessi fatto lo avresti progettato diversamente.

Ad esempio, nel caso in cui sia mattina e se è solo un animale, allora chiami mangiare, altrimenti se è un essere umano, allora chiama i primi lavamani, prendi il vestito e solo allora chiama mangia. Come gestire questi casi?

Ad esempio, in pseudocodice:

interface IEater { void Eat(); }
interface IMorningRoutinePerformer { void DoMorningRoutine(); }
interface IAnimal : IEater, IMorningPerformer;
interface IHuman : IEater, IMorningPerformer; 
{
  void WashHands();
  void GetDressed();
}

void MorningTime()
{
   IList<IMorningRoutinePerformer> items = Service.GetMorningPerformers();
   foreach(item in items) { item.DoMorningRoutine(); }
}

Ora si implementa IMorningPerformerper Animalper solo eseguire mangiare, e per Humanvoi anche implementare per lavare le mani e vestirsi. Il chiamante del tuo metodo MorningTime potrebbe fregarsene di meno se è umano o animale. Tutto ciò che vuole è la routine mattutina eseguita, che ogni oggetto fa mirabilmente grazie a OO.

Il polimorfismo muore.

O lo fa?

Devi scoprire il tipo di oggetto

Perché lo suppongo? Penso che questo potrebbe essere un presupposto sbagliato.

Esiste un approccio comune per gestire questi casi?

Sì, di solito viene risolto con una gerarchia di classi o interfacce attentamente progettata. Nota che nell'esempio sopra non c'è nulla che contraddica il tuo esempio mentre lo hai dato, tuttavia, probabilmente ti sentirai insoddisfatto, perché hai fatto alcune ipotesi in più che non hai scritto nella domanda al momento della scrittura e questi presupposti sono probabilmente violati.

È possibile andare in una tana di coniglio stringendo i tuoi presupposti e modificando la risposta per soddisfarli ancora, ma non penso che sarebbe utile.

Progettare gerarchie di buona classe è difficile e richiede molte informazioni sul tuo dominio aziendale. Per domini complessi si superano due, tre o anche più iterazioni, poiché perfezionano la comprensione di come interagiscono entità diverse nel proprio dominio aziendale, fino a quando non raggiungono un modello adeguato.

Ecco dove mancano esempi di animali semplicistici. Vogliamo insegnare in modo semplice, ma il problema che stiamo cercando di risolvere non è ovvio finché non si approfondisce, ovvero considerazioni e domini più complessi.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.