All'inizio, se si desidera seguire "Composizione su ereditarietà", più della metà delle soluzioni non si adatta perché si utilizza ancora l'ereditarietà in queste.
Realmente implementandolo con "Composition over Ereditarietà" esistono diversi modi, probabilmente ognuno con il proprio vantaggio e svantaggio. In un primo momento è possibile, ma non in C# al momento. Almeno non con qualche estensione che riscrive il codice IL. Un'idea è tipicamente usare mixin. Quindi hai interfacce e una classe Mixin. Un Mixin contiene fondamentalmente solo metodi che vengono "iniettati" in una classe. Non ne derivano. Così si potrebbe avere una classe come questo (tutto il codice è in pseudo-codice)
class RobotDog
implements interface IEat, IBark
implements mixin MEat, MBark
IEat
e IBark
fornisce le interfacce, mentre MEat
e MBark
sarebbero i mixins con una certa implementazione di default che si potrebbe iniettare. Un design come questo è possibile in JavaScript, ma non attualmente in C#. Ha il vantaggio che alla fine hai una classe RobotDog
che ha tutti i metodi di IEat
e IBark
con un'implementazione condivisa. E questo è anche uno svantaggio allo stesso tempo, perché crei grandi classi con molti metodi. Oltre a ciò ci possono essere conflitti di metodo. Ad esempio quando si vogliono iniettare due interfacce diverse con lo stesso nome/firma. Per quanto un simile approccio sembri prioritario, ritengo che gli svantaggi siano grandi e non incoraggerei un simile progetto.
Poiché C# non supporta direttamente Mixins, è possibile utilizzare i metodi di estensione per ricreare in qualche modo il progetto precedente. In modo da avere ancora IEat
e IBark
interfacce. E tu fornisci i metodi di estensione per le interfacce. Ma ha gli stessi svantaggi delle implementazioni di mixin. Tutti i metodi appaiono sull'oggetto, problemi con la collisione dei nomi dei metodi. Inoltre, l'idea di composizione è anche che potresti fornire diverse implementazioni. Potresti anche avere Mixin diversi per la stessa interfaccia. E per di più, i mix sono solo lì per una sorta di implementazione di default, l'idea è comunque che potresti sovrascrivere o modificare un metodo.
fare questo tipo di cose con metodo estensioni è possibile, ma non vorrei usare un tale progetto. Potresti teoricamente creare più spazi dei nomi diversi, in modo che, a seconda dello spazio dei nomi che carichi, ottieni un metodo di estensione diverso con un'implementazione diversa. Ma un tale disegno mi sembra più imbarazzante. Quindi non userei un tale disegno.
Il modo tipico per risolverlo è quello di prevedere i campi per ogni comportamento desiderato.Quindi il tuo RobotDog è simile a questo
class RobotDog(ieat, ibark)
IEat Eat = ieat
IBark Bark = ibark
Quindi questo significa. Hai una classe che contiene due proprietà Eat
e Bark
. Queste proprietà sono di tipo IEat
e IBark
. Se si desidera creare un'istanza RobotDog
, è necessario passare una specifica implementazione IEat
e IBark
che si desidera utilizzare.
let eat = new CatEat()
let bark = new DogBark()
let robotdog = new RobotDog(eat, bark)
Ora RobotDog Mangia come un gatto e abbaia come un cane. Puoi semplicemente chiamare ciò che il tuo RobotDog dovrebbe fare.
robotdog.Eat.Fruit()
robotdof.Eat.Drink()
robotdog.Bark.Loud()
Ora il comportamento del vostro RobotDog dipende completamente gli oggetti iniettato che si forniscono, mentre costruire il proprio oggetto. Puoi anche cambiare il comportamento in fase di esecuzione con un'altra classe. Se il vostro RobotDog è in un gioco e Barking viene aggiornato basta potrebbe sostituire Bark in fase di esecuzione con un altro oggetto e il comportamento che volete
robotdog.Bark <- new DeadlyScreamBarking()
In entrambi i casi mutando esso, o la creazione di un nuovo oggetto. Puoi usare un design mutevole o immutabile, dipende da te. Quindi hai la condivisione del codice. Almeno io mi piace molto di più lo stile, perché invece di avere un oggetto con centinaia di metodi hai fondamentalmente un primo strato con oggetti diversi che hanno ciascuna abilità separata in modo pulito. Se ad esempio aggiungi Moving alla tua classe RobotDog, potresti semplicemente aggiungere una proprietà "IMovable" e quell'interfaccia potrebbe contenere più metodi come MoveTo
, CalculatePath
, Forward
, SetSpeed
e così via. Sarebbero disponibili in modo pulito sotto robotdog.Move.XYZ
. Non hai problemi con i metodi di collisione. Ad esempio, potrebbero esserci metodi con lo stesso nome per ogni classe senza alcun problema. E in cima. Puoi anche avere più comportamenti dello stesso tipo! Ad esempio, salute e scudo potrebbero usare lo stesso tipo. Ad esempio un semplice tipo "MinMax" che contiene un valore minimo/massimo e valori e metodi per operare su di essi. Health/Shield fondamentalmente hanno lo stesso comportamento, e puoi facilmente usarne due nella stessa classe con questo approccio perché nessun metodo/proprietà o evento è in collisione.
robotdog.Health.Increase(10)
robotdog.Shield.Increase(10)
Il design precedente potrebbe essere leggermente modificato, ma non credo che lo rende migliore. Ma molte persone adottano senza cervello ogni modello o legge di progettazione con la speranza che faccia tutto automaticamente meglio. Quello che voglio riferire qui è spesso chiamato Law-of-Demeter
che penso sia terribile, specialmente in questo esempio. In realtà esiste un sacco di discussioni sul fatto che sia buono o no. Penso che non sia una buona regola da seguire, e in tal caso diventa anche ovvio. Se lo segui devi implementare un metodo per ogni oggetto che hai. Così, invece di
robotdog.Eat.Fruit()
robotdog.Eat.Drink()
di implementare metodi su RobotDog che chiama alcuni metodo sul campo mangiare, così con quello che hai fatto a finire?
robotdog.EatFruit()
robotdog.EatDrink()
È inoltre necessario ancora una volta a risolvere le collisioni come
robotdog.IncreaseHealt(10)
robotdog.IncreaseShield(10)
In realtà è sufficiente scrivere un sacco di metodi che solo i delegati ad alcuni altri metodi su un campo. Ma cosa hai vinto? Fondamentalmente niente. Hai appena seguito una regola senza cervello. Potresti teoricamente dire. Ma EatFruit()
potrebbe fare qualcosa di diverso o fare qualcosa di aggiuntivo prima di chiamare Eat.Fruit()
.Weel sì, potrebbe essere. Ma se vuoi altri comportamenti Eat diversi, devi semplicemente creare un'altra classe che implementa IEat
e assegni quella classe al robotdog quando la installi.
In questo senso, la legge di Demeter non è un esercizio di conteggio dei punti.
http://haacked.com/archive/2009/07/14/law-of-demeter-dot-counting.aspx/
Come conclusione. Se segui quel disegno, prenderei in considerazione l'utilizzo della terza versione. Utilizzare le proprietà che contengono gli oggetti Behavior e utilizzare direttamente tali comportamenti.
nel tuo esempio, il modo in cui abbaia Animale o Robot abbaia non cambia che sembra essere errato .... Il comportamento della corteccia sarebbe diverso in entrambi i casi, il che significa che il codice che scrivi nel metodo della corteccia sarebbe diverso ... – Viru
@Viru direi che le funzioni esatte nella domanda sono solo a scopo illustrativo e non devono essere prese alla lettera. Inoltre, sono sicuro che ci siano cani robot là fuori che abbaiano. – user1666620
Penso che il tuo caso sia un perfetto esempio di pattern composito. "Favorisci la composizione sull'ereditarietà": https://www.safaribooksonline.com/library/view/head-first-design/0596007124/ch01.html –