2016-02-12 22 views
11

Ispirato a a great video sull'argomento "Favorisci la composizione di un oggetto sull'ereditarietà" che utilizzava esempi JavaScript; Volevo provare in C# per testare la mia comprensione del concetto, ma non è andata bene come speravo.Come massimizzare il riutilizzo del codice in questa interfaccia rispetto all'Ereditarietà C# Esempio

/// PREMISE 
// Animal base class, Animal can eat 
public class Animal 
{ 
    public void Eat() { } 
} 
// Dog inherits from Animal and can eat and bark 
public class Dog : Animal 
{ 
    public void Bark() { Console.WriteLine("Bark"); } 
} 
// Cat inherits from Animal and can eat and meow 
public class Cat : Animal 
{ 
    public void Meow() { Console.WriteLine("Meow"); } 
} 
// Robot base class, Robot can drive 
public class Robot 
{ 
    public void Drive() { } 
} 

Il problema è che voglio aggiungere classe RobotDog che può corteccia e Drive, ma non mangiare.


Prima soluzione era quella di creare RobotDog come sottoclasse di Robot,

public class RobotDog : Robot 
{ 
    public void Bark() { Console.WriteLine("Bark"); } 
} 

ma per dargli una funzione Bark abbiamo dovuto copiare e incollare la funzione Bark del Cane così ora abbiamo il codice duplicato.


La seconda soluzione era quella di creare una classe super-comune con un metodo di corteccia che poi entrambi le classi di animali e robot ereditati dal

public class WorldObject 
{ 
    public void Bark() { Console.WriteLine("Bark"); } 
} 
public class Animal : WorldObject { ... } 
public class Robot : WorldObject { ... } 

Ma ora ogni animale e ogni robot avrà un metodo di Bark , di cui la maggior parte non ha bisogno. Continuando con questo modello, le sottoclassi saranno cariche di metodi di cui non hanno bisogno.


La terza soluzione era quella di creare un'interfaccia IBarkable per le classi che abbaiare

public interface IBarkable 
{ 
    void Bark(); 
} 

E attuarlo nelle classi Cane e RobotDog

public class Dog : Animal, IBarkable 
{ 
    public void IBarkable.Bark() { Console.WriteLine("Bark"); } 
} 
public class RobotDog : Robot, IBarkable 
{ 
    public void IBarkable.Bark() { Console.WriteLine("Bark"); } 
} 

Ma ancora una volta che abbiamo duplicato codice!


Il quarto metodo è stato utilizzato per una volta l'interfaccia IBarkable, ma creare una classe di supporto corteccia che poi ciascuna delle implementazioni di interfacce Cane e RobotDog rimettere in.

Questo è il come il metodo migliore (e ciò che il video sembra raccomandare), ma potrei anche vedere un problema nel progetto che si riempie di aiutanti.


Un quinto ha suggerito (hacky?) Soluzione era quella di appendere un metodo di estensione fuori un'interfaccia IBarkable vuoto, in modo che se si implementa IBarkable, allora si può Corteccia

public interface IBarker { } 
public static class ExtensionMethods 
{ 
    public static void Bark(this IBarker barker) { 
     Console.WriteLine("Woof!"); 
    } 
} 

Un sacco di domande con risposta simile su questo sito, così come gli articoli che ho letto, sembrano raccomandare l'uso delle classi abstract, tuttavia, non avrebbero gli stessi problemi della soluzione 2?


Qual è il miglior modo orientato agli oggetti per aggiungere la classe RobotDog a questo esempio?

+1

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

+0

@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

+0

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 –

risposta

3

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.

+0

Finora sembra che l'incapsulamento sia la strada da percorrere. – chriszumberge

2

Penso che questo sia più un dilemma concettuale piuttosto che un problema di composizione.

Quando si dice: E attuarlo nelle classi Cane e RobotDog

public class Dog : Animal, IBarkable 
{ 
    public void IBarkable.Bark() { Console.WriteLine("Bark"); } 
} 
public class RobotDog : Robot, IBarkable 
{ 
    public void IBarkable.Bark() { Console.WriteLine("Bark"); } 
} 

Ma ancora una volta abbiamo codice duplicato!

Se Dog e RobotDog hanno la stessa implementazione di Bark(), dovrebbero ereditare dalla classe Animal. Ma se le loro implementazioni di Bark() sono diverse, ha senso derivare dall'interfaccia di IBarkable. Altrimenti, dov'è la differenza tra Dog e RobotDog?

+0

Fare in modo che RobotDog sia ereditato dalla classe Animal non può essere una soluzione perché abbiamo gli stessi problemi. Se lo facessimo, ora RobotDog ha un metodo Eat() della classe Animal che non dovrebbe avere. Inoltre ora manca il metodo Drive() dalla classe Robot. Dovremmo ora copiare il metodo Drive() nella classe RobotDog (copia/incolla codice duplicato) o implementare un'interfaccia IDrivable (stessi problemi di IBarkable con duplicate implementazioni identiche tra le classi) – chriszumberge

+2

Lo scopo principale dell'ereditarietà non è il codice- condivisione. Quelle idee ridicole di "hey due classi usano lo stesso metodo, metti in classe e derivano da esso" conducono agli alberi di eredità orribili esatti che nessuno può veramente capire ad un certo punto nel tempo. L'ereditarietà è la creazione di sottotipi, che possono condividere codice comune è "bello avere", non si eredita allo scopo di condividere alcune linee di codice. Dovresti seguire LSP. E il problema di Circle-Elipse lo spiega ulteriormente perché non si dovrebbe ereditare solo per la condivisione di un codice: https://en.wikipedia.org/wiki/Circle-ellipse_problem –

+1

@chriszumberge Penso che il problema è che si sta cercando di combinare due diversi concetti animale e robot. RobotDog e Dog non abbaiano allo stesso modo e sono essenzialmente due oggetti distinti indipendentemente dal comportamento "barkable". Essere d'accordo? – mojorisinify