Possiamo sostituire completamente l'ereditarietà usando il modello di strategia e l'iniezione di dipendenza?


10

Per esempio:

var duckBehaviors = new Duckbehavior();
duckBehaviors.quackBehavior = new Quack();
duckBehaviors.flyBehavior = new FlyWithWings();
Duck mallardDuck = new Duck(DuckTypes.MallardDuck, duckBehaviors)

Poiché la classe Duck contiene tutti i comportamenti (astratto), non sembra necessario creare una nuova classe MallardDuck(che si estende Duck).

Riferimento: Head First Design Pattern, Chapter 1.


Quali sono i tipi di Duckbehavior.quackBehaviore altri campi "nel tuo codice?
max630,

Non vi è alcuna iniezione di dipendenza nel tuo esempio.
David Conrad,

11
L'ereditarietà è fantastica quando sei uno sviluppatore di livello medio-basso perché c'è una lunga storia di trucchi di refactoring e modelli di progettazione intorno ad esso. Ma gli sviluppatori più esperti con cui ho parlato preferiscono gerarchie ereditarie molto superficiali basate sulla composizione ogni volta che è possibile. L'ereditarietà può causare un accoppiamento più stretto del necessario e può rendere difficili le modifiche.
Mark Rogers,


5
@DavidConrad: l'OP non sta eseguendo le nuove operazioni all'interno della classe . DI / IoC riguarda l'iniezione di dipendenze, non di contenitori o di non usare mai "nuovo"; questa è una preoccupazione interamente ortogonale. Inoltre, devi sempre cambiare il codice da qualche parte, sia esso la radice della composizione o un file di configurazione. Il controllo è invertito qui all'interno del tipo Duck, poiché la cosa che controlla la creazione delle dipendenze non è il Duck Ctor, ma un contesto esterno; essendo questo un esempio di giocattolo dato per chiarezza, è perfettamente bene che il contesto esterno sia rappresentato solo dal codice chiamante.
Filip Milovanović,

Risposte:


21

Certo, ma chiamiamo quella composizione e delega . Il modello di strategia e l'iniezione di dipendenza potrebbero sembrare strutturalmente simili ma i loro intenti sono diversi.

Il modello di strategia consente la modifica runtime del comportamento sotto la stessa interfaccia. Potrei dire a un'anatra selvatica di volare e guardarla volare con le ali. Quindi sostituiscilo con un'anatra pilota e guardalo volare con le compagnie aeree Delta. Farlo mentre il programma è in esecuzione è una cosa del modello di strategia.

L'iniezione di dipendenza è una tecnica per evitare dipendenze di codifica rigida in modo che possano cambiare in modo indipendente senza richiedere che i client vengano modificati quando cambiano. I clienti esprimono semplicemente i loro bisogni senza sapere come verranno soddisfatti. Pertanto, il modo in cui vengono incontrati viene deciso altrove (in genere nella parte principale). Non hai bisogno di due anatre per usare questa tecnica. Solo qualcosa che usa un'anatra senza sapere o preoccuparsi di quale anatra. Qualcosa che non costruisce l'anatra o non va a cercarla ma è perfettamente felice di usare qualunque anatra le passi.

Se ho una classe di anatre di cemento posso farla implementare il suo comportamento di volo. Potrei anche far cambiare i comportamenti da volare con le ali a volare con il Delta in base a una variabile di stato. Quella variabile potrebbe essere un valore booleano, un int o potrebbe essere FlyBehaviorun flymetodo che ha uno stile di volo senza che io debba testarlo con un if. Ora posso cambiare gli stili di volo senza cambiare il tipo di anatra. Ora i germani reali possono diventare piloti. Questa è composizione e delega . L'anatra è composta da un FlyBehavior e può delegare ad esso richieste di volo. Puoi sostituire tutti i tuoi comportamenti da anatra in questo modo o tenere qualcosa per ogni comportamento o qualsiasi combinazione in mezzo.

Questo ti dà tutti gli stessi poteri che l'eredità ha tranne uno. L'ereditarietà ti consente di esprimere i metodi Duck che stai sostituendo nei sottotipi Duck. La composizione e la delega richiedono che l'Anatra deleghi esplicitamente ai sottotipi dall'inizio. Questo è molto più flessibile ma comporta una maggiore digitazione della tastiera e Duck deve sapere che sta accadendo.

Tuttavia, molte persone credono che l'eredità debba essere progettata esplicitamente fin dall'inizio. E che se non lo è stato, dovresti contrassegnare le tue classi come sigillate / definitive per impedire l'eredità. Se lo consideri, l'eredità non ha davvero alcun vantaggio rispetto alla composizione e alla delega. Perché in entrambi i casi devi progettare per l'estensibilità dall'inizio o essere disposto a demolire le cose in seguito.

Abbattere le cose è in realtà un'opzione popolare. Basta essere consapevoli del fatto che ci sono casi in cui è un problema. Se hai distribuito in modo indipendente librerie o moduli di codice che non intendi aggiornare con la prossima versione, puoi finire per occuparti di versioni di classi che non sanno nulla di ciò che stai facendo ora.

Mentre essere disposti a strappare le cose in un secondo momento può liberarti dalla progettazione eccessiva, c'è qualcosa di molto potente nel poter progettare qualcosa che usa un'anatra senza dover sapere cosa farà effettivamente l'anatra quando viene utilizzata. Quello non sapere è roba potente. Ti consente di smettere di pensare alle anatre per un po 'e pensare al resto del codice.

"Possiamo" e "dovremmo" sono domande diverse. Favorire la composizione sull'ereditarietà non dice mai di usare l'eredità. Ci sono ancora casi in cui l'eredità ha più senso. Ti mostrerò il mio esempio preferito :

public class LoginFailure : System.ApplicationException {}

L'ereditarietà consente di creare eccezioni con nomi più specifici e descrittivi in ​​una sola riga.

Prova a farlo con la composizione e avrai un casino. Inoltre, non vi è alcun rischio del problema dell'ereditarietà perché non vi sono dati o metodi per riutilizzare e incoraggiare il concatenamento dell'ereditarietà. Tutto ciò aggiunge un buon nome. Non sottovalutare mai il valore di un buon nome.


1
" Non sottovalutare mai il valore di un buon nome ". Il tempo dei nuovi vestiti dell'Imperatore: LoginExceptionnon è un buon nome. È la classica "denominazione dei puffi" e, se lo porto via Exception, tutto ciò che ho è Loginche non mi dice nulla di ciò che è andato storto.
David Arno,

Purtroppo siamo bloccati con la ridicola convenzione di mettere Exceptionfine a ogni classe di eccezione, ma per favore non confondere "bloccato con esso" con "buono".
David Arno,

@DavidArno Se puoi suggerire un nome migliore, sono tutto orecchi. Qui abbiamo il vantaggio di non essere intrappolati nelle convenzioni di una base di codice esistente. Se avessi il potere di cambiare il mondo con uno schiocco di dita, come lo chiameresti?
candied_orange,

Mi piacerebbe nome loro, StackOverflow, OutOfMemory, NullReferenceAccess, LoginFailureecc Fondamentalmente, prendono "Eccezione" fuori il nome. E, se necessario, correggili in modo che descriva cosa è andato storto.
David Arno,

@DavidArno al tuo comando.
candied_orange,

7

Puoi sostituire quasi ogni metodologia con qualsiasi altra metodologia e produrre comunque software funzionante. Tuttavia, alcuni si adattano meglio a un problema particolare rispetto ad altri.

Dipende da molte cose quale è preferibile. Arte precedente nell'applicazione, esperienza nel team, sviluppi futuri previsti, preferenza personale e quanto sarà difficile per un nuovo arrivato capovolgerlo, per citarne alcuni.

Man mano che diventerai più esperto e lotterai più spesso con le creazioni di altre persone, probabilmente metterai maggiormente l'accento sull'ultima contemplazione.

L'ereditarietà è ancora uno strumento di modellazione valido e solido che non è necessariamente sempre il più flessibile, ma offre una forte guida a nuove persone che potrebbero essere grate per la chiara mappatura al dominio problematico.


-1

L'ereditarietà non è così importante come si pensava una volta. È ancora importante e rimuoverlo sarebbe un brutto errore.

Un esempio estremo è Objective-C in cui tutto è una sottoclasse di NSObject (il compilatore in realtà non ti consente di dichiarare una classe che non ha una baseclass, quindi tutto ciò che non è incorporato nel compilatore deve essere una sottoclasse di qualcosa integrato nel compilatore). Ci sono molte cose utili integrate in NSObject che non dovresti perdere.

Un altro esempio interessante è UIView, la classe di base "view" in UIKit per lo sviluppo di iOS. Questa è una classe che è da un lato un po 'come una classe astratta che dichiara funzionalità che le sottoclassi dovrebbero compilare, ma è anche utile da sola. Ha sottoclassi fornite da UIKit, che vengono spesso utilizzate così come sono. C'è una composizione da parte dello sviluppatore che installa viste secondarie in una vista. E ci sono spesso sottoclassi definite dallo sviluppatore, che spesso usano la composizione. Non esiste una singola regola rigida o addirittura regole, si utilizza ciò che si adatta meglio alle proprie esigenze.


Il tuo "esempio estremo" è approssimativamente il modo in cui funzionano i più moderni linguaggi OO: sottoclassi di Python type, da cui ogni non primitivo in Java eredita Object, tutto in JavaScript ha un prototipo che termina con Object, ecc.
Delioth

-1

[Rischio una risposta sfacciata alla domanda principale.]

Possiamo sostituire completamente l'ereditarietà usando il modello di strategia e l'iniezione di dipendenza?

Sì ... tranne che il modello di strategia stesso utilizza l'eredità. Il modello di strategia funziona sull'ereditarietà dell'interfaccia. La sostituzione dell'eredità con la composizione + strategia sposta l'eredità in un posto diverso. Tuttavia, tale sostituzione spesso vale la pena fare, perché consente di distanziare le gerarchie .


2
" Il modello di strategia funziona sull'ereditarietà dell'interfaccia ". Ricorda, il modello di strategia è un modello di progettazione; non un modello di implementazione. Quindi può essere implementato usando le interfacce, ma può anche essere implementato usando ad esempio un hash / dizionario di funzioni.
David Arno,

3
L'ereditarietà dell'interfaccia non è affatto eredità, è solo un tipo di contratto o classificatore. Puoi usare i delegati anche per il modello di strategia (preferisco usarli).
Deepak Mishra l'

Più uno, amico: ... funziona sull'eredità dell'interfaccia , complimenti per l'uso corretto del termine generale "interfaccia". Penso che il design di classe scadente o incompetente sia la ragione principale per cui viene posta questa domanda. Per spiegare perché / come dovrebbe essere un libro; il diavolo è nei dettagli. Ho lavorato con disegni ereditari che erano una gioia da vedere e allo stesso tempo un'eredità profonda che era un inferno. Ho visto il interfacedesign craptastic corretto con l'eredità. Il problema nascosto qui è l'implementazione incoerente del comportamento morphing dipendente. P, S. eredità e interfaccia non si escludono a vicenda,
radarbob

@DavidArno comment : Questo commento andrebbe bene se allegato alla domanda OP. La domanda di OP è tecnicamente dichiarata in modo errato, ma la otteniamo ancora. Il problema non è questa risposta particolare.
radarbob

@DeepakMishra commento: . Un'interfaccia è costituita da tutti i membri pubblici di una classe. Sfortunatamente il significato di "interfaccia" è sovraccarico. Dobbiamo stare attenti a distinguere il significato generale con la parola chiave di un linguaggio di programmazioneinterface
radarbob

-2

No. Se un'anatra selvatica richiede parametri diversi per istanziare rispetto ad altri tipi di anatre, sarebbe un incubo cambiare. Dovresti anche essere in grado di creare un'istanza di un'anatra? È più un'anatra selvatica o un'anatra mandarina o qualunque altro tipo di anatra tu abbia.

Inoltre potrei volere un po 'di logica attorno ai tipi, quindi una gerarchia dei tipi potrebbe essere migliore.

Ora se il riutilizzo del codice diventa problematico per alcune funzionalità, potremmo comporlo passando le funzioni attraverso il costruttore.

Ancora una volta, dipende davvero dal tuo caso d'uso. Se hai solo un'anatra e un'anatra selvatica, una gerarchia di classi è una soluzione molto più semplice.

Ma ecco un esempio in cui ti piacerebbe utilizzare il modello di strategia: se hai classi di clienti e desideri passare una strategia di fatturazione (interfaccia) che potrebbe essere la strategia di fatturazione dell'happy hour o una strategia di fatturazione regolare, potresti passarla nel costruttore. In questo modo non è necessario creare due diverse classi di clienti e non è necessaria una gerarchia. Hai solo una classe cliente.

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.