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. È grandioso Esempio:

Hai una collezione di animali e chiedi a tutti gli animali la funzione eat e non ti importa se si tratta di un cane che mangia o di un gatto. Ma nella stessa gerarchia di classi hai degli animali che hanno addizionali, oltre che ereditati e implementati dalla classe Animal , ad es. makeEggs , getBackFromTheFreezedState e così via. Pertanto, in alcuni casi, nella tua funzione potresti voler conoscere il tipo specifico per chiamare comportamenti aggiuntivi.

Ad esempio, se è mattina e se è solo un animale, allora chiami eat , altrimenti se è un umano, quindi chiama prima washHands , getDressed e solo allora chiama eat . Come gestire questi casi? Il polimorfismo muore. Hai bisogno di scoprire il tipo di oggetto, che suona come un odore di codice. C'è un approccio comune per gestire questi casi?

    
posta Narek 19.02.2018 - 14:58
fonte

4 risposte

18

Dipende. Sfortunatamente non esiste una soluzione generica. Pensa alle tue esigenze e cerca di capire cosa dovrebbero fare queste cose.

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

Se vuoi differenziare gli animali, non devi memorizzarli indiscriminatamente in un elenco.

Se non funziona nient'altro, allora potresti provare il Pattern visitatori , che è < a href="https://lostechies.com/derekgreer/2010/04/19/double-dispatch-is-a-code-smell/"> tipo-di un hack per consentire il tipo di un dispacciamento dinamico in cui è possibile inviare un visitatore che riceverà callback esatti tipo dagli animali. Vorrei sottolineare tuttavia che questa dovrebbe essere l'ultima risorsa se tutto il resto fallisce.

    
risposta data 19.02.2018 - 16:03
fonte
33

Questa è una buona domanda ed è il tipo di problema che molta gente prova a capire come usare OO. Penso che molti sviluppatori lottino con questo. Vorrei poter dire che la maggior parte lo supera, ma non sono sicuro che sia così. La maggior parte degli sviluppatori, nella mia esperienza, ha finito con l'utilizzo di pseudo-OO property bags .

In primo luogo, vorrei essere chiaro. Non è colpa tua Il modo in cui l'OO viene tipicamente insegnato è altamente imperfetto. L'esempio di Animal è il principale autore del reato, IMO. Fondamentalmente, diciamo, parliamo di oggetti, cosa possono fare. Un Animal può eat() e può speak() . Super. Ora crea alcuni animali e digita il codice di 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 tipo Animal . Sono sicuro che esista ma discutiamo di qualcosa che ritengo sia più facile ragionare: una simulazione del traffico. Supponiamo di voler modellare il traffico in vari scenari. Ecco alcune cose di base che dobbiamo essere in grado di farlo.

Vehicle
Road
Signal

Possiamo approfondire con tutti i tipi di cose pedoni e treni, ma lo terremo semplice.

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

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

Probabilmente è troppo semplice ma è un inizio. Adesso. Che dire di tutte le altre cose che un veicolo potrebbe fare? Possono spegnere una strada e finire in un fosso. È quella parte della simulazione? No. Non ne ho bisogno. Alcune macchine e autobus hanno l'idraulica che consente loro di rimbalzare o inginocchiarsi rispettivamente. È quella parte della simulazione? No. Non ne ho bisogno. La maggior parte delle auto brucia la benzina. Alcuni no. La centrale è parte della simulazione? No. Non ne ho bisogno. Dimensioni della ruota? Non ne ho bisogno. Navigazione GPS? Sistema di infotainment? Non ho bisogno di em.

Devi solo definire i comportamenti che intendi utilizzare. A tal fine, penso che sia spesso preferibile 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. È così che sai quali metodi hai bisogno sulla tua interfaccia. Quindi, una volta che hai fatto ciò, vai e inizia a definire le classi che implementano questi comportamenti. I comportamenti non utilizzati sono irrilevanti e non devono essere definiti.

L'intero punto di OO è che è possibile aggiungere nuove implementazioni di queste interfacce in seguito senza modificare il codice chiamante. L'unico modo in cui funziona è se le esigenze del codice chiamante determinano cosa va nell'interfaccia. Non c'è modo di definire tutti i comportamenti di tutte le possibili cose che potrebbero essere pensate in seguito.

    
risposta data 19.02.2018 - 17:59
fonte
9

TL; DR:

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

Rimaniamo prima con il tuo esempio di eat() .

È una proprietà dell'essere umano che, come precondizione al 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 devi dirlo per lavarsi le mani e 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:

Poiché un'istanza Human non mangerà senza le precondizioni, il metodo Human eat() fa washHands() e getDressed() se ciò non è stato fatto. Non dovrebbe essere il tuo lavoro come il chiamante eat() a conoscere questa particolarità. L'alternativa dell'intelligente umano sarebbe quella di gettare un'eccezione ("Non sono preparato a mangiare!") Se le condizioni preliminari non sono soddisfatte, lasciandoti frustrato, ma almeno informato che mangiare non ha funzionato.

Che dire di makeEggs() ?

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

    
risposta data 19.02.2018 - 16:09
fonte
2

La risposta è abbastanza semplice.

How to handle objects that can do more than you expect?

Non è necessario gestirlo perché non servirebbe a nulla. Un'interfaccia di solito è progettata a seconda di come verrà utilizzata. Se la tua interfaccia non definisce le mani di lavaggio, allora non ti interessa come il chiamante dell'interfaccia; se l'avessi fatto lo avresti progettato diversamente.

For example, in case it is morning time and if it is just an animal then you call eat, otherwise if it is a human, then call first washHands, getDressed and only then call eat. How to handle this cases?

Ad esempio, in pseudocode:

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 implementi IMorningPerformer per Animal solo per mangiare e per Human la impili anche per lavarti le mani e vestirti. Il chiamante del tuo metodo MorningTime potrebbe interessare di meno se si tratta di un animale o di un animale. Tutto ciò che vuole è la routine mattutina, che ogni oggetto fa mirabilmente grazie a OO.

Polymorphism dies.

O lo fa?

You need to find out the type of the object

Perché lo stai assumendo? Penso che questo potrebbe essere un assunto sbagliato.

Is there a common approach to handle this cases?

Sì, di solito è risolto con una gerarchia di classi o interfacce attentamente progettata. Nota che nell'esempio sopra non c'è nulla che contraddica il tuo esempio come tu l'hai dato, tuttavia, probabilmente ti sentirai insoddisfatto, perché hai fatto alcune ipotesi che non hai scritto nella domanda al momento della scrittura e queste supposizioni sono probabilmente violate.

È possibile andare in una tana del coniglio stringendo le tue ipotesi e io modificando la risposta per soddisfarle, ma non penso che sarebbe utile.

La progettazione di gerarchie di buone classi è difficile e richiede molte informazioni sul tuo dominio aziendale. Per domini complessi, si passano più di due, tre o anche più iterazioni, dal momento che perfezionano la loro comprensione di come interagiscono entità diverse nel loro dominio aziendale, fino a quando non arrivano a 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 fino a quando non vai più a fondo, cioè ci sono considerazioni e domini più complessi.

    
risposta data 20.02.2018 - 04:35
fonte