Composizione sull'ereditarietà, perché non entrambi?

2

Ho questo scenario fuori dal contesto , dove ciò che ritengo sia una buona pratica mi lascia in una situazione di implementazione di un'interfaccia e utilizzo della composizione per eseguire l'implementazione .

Immagina quanto segue:

Ho un Character con Health e Mana , così:

interface ICharacter
{
    Health Health { get; }
    Mana Mana { get; }
}

class Character : ICharacter
{
    public Health Health { get; private set; }
    public Mana Mana { get; private set; }

    public Character(Health health, Mana mana)
    {
        this.Health = health;
        this.Mana = mana;
    }
}

Decido quindi che Health e Mana devono essere combinati in un unico oggetto, poiché appartengono insieme.

interface IResource
{
    Health Health { get; }
    Mana Mana { get; }
}

Cambio il mio Character in

interface ICharacter
{
    IResource Resource { get; }
}

class Character : ICharacter
{
    public IResource Resource { get; private set; }

    public Character(IResource resource)
    {
        this.Resource = resource;
    }
}

Tuttavia, in modo che i client di Character possano accedere alla salute di un personaggio senza dire character.Health.Current e "violando" il principio di minima conoscenza voglio Character per fornire queste informazioni. E per migliorare l'incapsulamento non vorrei più rivelare come Character memorizza il suo Health e Mana , in questo modo:

interface ICharacter : IResource
{
}

class Character : ICharacter
{
    private IResource resource;

    public Health Health
    {
        get
        {
            return resource.Health;
        }
    }

    public Mana Mana
    {
        get
        {
            return resource.Mana;
        }
    }

    public Character(IResource resource)
    {
        this.resource = resource;
    }
}

Ritorna il mio Character ad avere Health e Mana che mi piacciono, ma Character ora sta implementando la stessa interfaccia di cui sono composto.

Nell'implementazione, sembra molto simile al Pattern Decoratore , ma nell'intenzione non hanno nulla in comune.

Domande

  1. Questo modello / antipattern di implementare la stessa interfaccia di una classe è composto da riconosciuto e, in tal caso, come si chiama?

  2. Considera questa idea una buona idea solo per avere una proprietà su IResource? - Perché? / perché no?

  3. Qualche situazione da diffidare, dove questo è un no-go definitivo?

Sidenote - ICharacter avrebbe sicuramente più metodi, ma per accorciarlo e tenerlo conciso, ho incluso solo la parte rilevante qui.

    
posta Chris Wohlert 12.04.2017 - 10:37
fonte

4 risposte

8

Composition over Inheritance, why not both?

Il principio viene correttamente indicato come " preferisci composizione sull'ereditarietà". Nessuno ha detto che non potevi fare entrambe le cose. A volte devi fare entrambe le cose. Ma data una scelta, la composizione è solitamente la migliore.

La tua IResource mi ricorda di modelli diversi da quelli che hai menzionato. Oggetto Parametro semplifica la costruzione di un oggetto raggruppando insieme le sue dipendenze.

Attenzione, preso a un livello estremo questo sa di servizio di localizzazione che può essere cattivo in alcune situazioni.

Il rispetto della Legge di Demeter è una buona cosa se la capisci bene. Ti sta causando un problema qui, ma è un buon problema. Ti costringe a semplificare l'accesso a salute e mana fino a quando è ovvio che il tuo Character non sta facendo altro che l'aggregazione.

Ciò che manca è l'astrazione. Certo, hai promesso che Character avrebbe avuto più metodi, ma per quanto vedo qui Character non sta aggiungendo altro che indiretta non necessaria.

In questo momento Character sembra un oggetto con valore morto cerebrale (come String). Se questo è ciò che sarà, allora dargli le proprie copie di questi valori e appiattirlo.

Se Character sarà un oggetto comportamentale, mi aspetterei che Health venga nascosto dall'incapsulamento e interagito con metodi come Wound(int) e Die() non GetHealth() . Questo segue Racconta, non chiedere . Lo stato di salute di un personaggio non è affare di nessun altro. Questo è un vero incapsulamento.

In entrambi i casi, la classe Character non dovrebbe essere una breve sosta sulla gestione dei dettagli dei personaggi. Questo dovrebbe essere IL posto per risolverli. Non farmi scavare oltre.

    
risposta data 12.04.2017 - 17:25
fonte
4

Quali benefici stai ottenendo dal raggruppamento di salute e mana insieme? Questi mi sembrano valori e attributi separati, ma indipendentemente dal fatto che siano valori o oggetti potresti invece inserirli come proprietà. Questi sono pensati per essere facilmente controllati contro il personaggio.

Penso che tu abbia avuto ragione la prima volta. Se il tuo mana / salute potrebbe avere comportamenti o regole che danno luogo ad essere più di un valore, penso che dovresti programmare contro la loro interfaccia (che ti permette di prenderti gioco quando l'unità prova i comportamenti del personaggio):

interface ICharacter
{
    IMana Mana { get; }
    IHealth Health { get; }
}

public class Character : ICharacter
{
    public IMana Mana { get; }
    public IHealth Health { get };

    public Character(IMana mana, IHealth health)
    {
        Mana = mana;
        Health = health;
    }
}

Se sono solo valori, basta che siano valori. Con l'approccio al valore non è necessario nemmeno fornirli al costruttore. Puoi semplicemente usare i valori di default ragionevoli. Ora, quando l'incapsulamento diventa utile, puoi eseguire il bake-nella logica di cura e mana all'interno di un metodo esposto pubblicamente e il codice chiamante non può modificare la salute e il mana senza utilizzare i metodi esposti:

interface ICharacter
{
    int Mana { get; }
    int Health { get; }
    void Heal(int numPotions);
    void RejuvenateMana(int numPotions);
}

public class Character : ICharacter
{
    public int Mana { get; private set; } // default is 0
    public int Health { get; private set; } = 100;

    public void Heal(int numPotions) 
    {
        Health += numPotions;//todo: add real logic
    }

    public void RejuvenateMana(int numPotions)
    {
        Mana += numPotions;//todo: add real logic
    }

}

Una volta capito che potresti avere comportamenti e regole diversi, probabilmente vorrai aggiungere quelle regole come dipendenze o avere un altro modo per variare i comportamenti tra i tipi di carattere. A seconda del modo in cui tutto funziona insieme, potresti scoprire che la guarigione, il ringiovanimento, il danno, ecc potrebbero essere gestiti da un oggetto diverso e in tal caso vorrai che la salute / mana sia proprietà assegnabile pubblicamente se ICharacter verrà interpretato da qualcosa altro. Altrimenti potrebbe essere ragionevole fornire le dipendenze che calcolano la salute / mana come dipendenza.

Would you consider this good a idea over just having a property to IResource? - Why? / why not?

Questi mi sembrano due attributi separati di carattere. Penso che la loro combinazione lo farebbe solo per orgranizzare le cose, e farebbe sì che il codice chiamante debba scavare inutilmente per ottenere informazioni che avrebbero potuto essere prontamente disponibili.

Is this pattern / antipattern of implementing the same interface that a class is composed of recognized, and if so, what is it called?

Chiamerei questo over-engineering e over-generalization. IResource è diventato una cosa talmente generica che in questo caso non ha davvero un buon significato singolo e rompe il principio di Segregazione dell'Interfaccia a mio parere essendo troppo generalizzato e non distrutto a sufficienza. Ciò che si sta facendo implementando un'interfaccia e prendendo dipendenze che implementano la stessa interfaccia è comune anche se in alcune circostanze, in alcuni linguaggi di programmazione tutti gli oggetti ereditano da un qualche tipo di Object o nel caso di Objective-C c'è un La classe NSObject eredita da e un protocollo NSObject (interfaccia). Questo schema è stato utilizzato prima per preparare le cose in ogni cosa, ma di solito funziona solo nei casi in cui Tutto avrà bisogno di avere le stesse esatte proprietà (es .: i componenti dell'interfaccia avranno sempre posizioni x, y, ecc.)

Any situations to be wary off, where this is a definitive no-go?

Come ho detto prima, mi sento come se ciò rompesse l'ISP. I clienti non dovrebbero dipendere da cose di cui non hanno bisogno. Se il carattere è una risorsa, un personaggio non può cambiare indipendentemente da IResource poiché ICharacter eredita tutto nell'interfaccia IResource. E ICharacter potrebbe potenzialmente avere caratteristiche IResource che non è necessario se IResource viene utilizzato in modo così generico e vengono aggiunti più tratti che non appartengono a ICharacter.

Modifica: mi piacerebbe molto vedere come interagirà la classe Personaggio con altre classi. Ad esempio se c'era una classe BattleCalculator che occupa 2 caratteri e guarda i loro attributi per vedere quanti danni saranno applicati in base a attributi come Salute, Agilità, Arma, ecc. Potrebbe non essere necessario conoscere il mana (supponendo che il mana sia usato solo per cambiare gli altri attributi del personaggio). In tal caso puoi dividere ulteriormente le interfacce in modo che le cose relative al mana si trovino in un'interfaccia diversa e il calcolatore di battaglia non ne abbia bisogno, e questo presuppone che tutti i personaggi abbiano Mana:

public interface IManaBearer 
{ 
    IMana Mana { get; } 
}
public interface ICharacter { ... }
public class Character: IManaBearer, ICharacter {...}

public class CharacterBattleCalculator
{
    public CharacterBattleCalculator(ICharacter player1, ICharacter player2)
    {
        ...
    }
    ...
}
    
risposta data 12.04.2017 - 16:24
fonte
3

Is this pattern / antipattern of implementing the same interface that a class is composed of recognized, and if so, what is it called?

È un Proxy o Dectorator . In genere i proxy fanno riferimento a Wrappers o Adattatori che inoltrano semplicemente le chiamate di metodo a un'altra classe o istanza, ad esempio una proprietà interna. I decoratori sono fondamentalmente proxy che vengono con i metodi aggiuntivi o "sostituzioni".

Would you consider this good a idea over just having a property to IResource? - Why? / why not?

In sé e per sé, è un'idea neutra . Si tratta della nomenclatura. Se ICharacter è un IResource , allora è una buona idea. Se solo ha un IResource , è una pessima idea.

Any situations to be wary off, where this is a definitive no-go?

Oltre a quando non è un adattamento semantico , è anche una cattiva idea quando il wrapping sarà difficile da mantenere. Questo potrebbe essere il caso se l'interfaccia interna è volatile (ancora in fase di sviluppo). ... Personalmente, eviterei anche un proxy / decoratore se il "principio di minima conoscenza" è la mia unica giustificazione, specialmente per le grandi interfacce.

Composition over Inheritance, why not both?

Nel tuo caso, perché in realtà non stai facendo entrambi!

Le classi sono ereditate. Interfacce sono implementate . Quindi stai facendo "composizione e implementazione di interfacce ". E questa è una cosa normale e normale da fare.

Questo è ciò che assomigliano a "entrambi" composizione ed eredità:

class A : B
{
  public C Prop;
}

La classe A eredita da B ed è composta da C .

Quindi, in generale, perché non fare entrambi?

Nessun motivo. Se ha senso che A erediti da B e usi C nella sua composizione, fallo. Se non lo è, no.

Fai semplicemente ciò che ha senso.

Tempo di consigli sui bonus! per il tuo e i tuoi amici, sappi perché i principi esistono. Preferendo la composizione rispetto all'ereditarietà ha vantaggi e svantaggi . E francamente, i vantaggi del principio di minima conoscenza sono traballanti nella migliore delle ipotesi (secondo me). Ma soprattutto, se C del nostro esempio può fare cose che non vogliamo che i consumatori di A facciano direttamente, C dovrebbe essere privato. Tuttavia, se creiamo un wrapper per ogni metodo su C , dovremmo solo rendere C pubblico.

    
risposta data 12.04.2017 - 16:52
fonte
0

Due note per continuare:

  1. Se qualcuno conosce ICharacter allora conosce Iresource, quindi esponendo gli interni della classe di risorse non ha reso la classe che usa meno il carattere.

  2. Penso che tu abbia reso più confuso il fatto che qualcuno conosca questa interfaccia di IResource, che è stata creata perché hai detto che questi due pezzi di dati si incontrano, e quindi gli permetti di accedere a ogni singolo dato individualmente. Devi decidere - o entrambi i pezzi si incontrano (e poi sono anche esposti insieme), o che crea da quando ognuno di loro senza l'altro (e poi non li ha fatti fare un corso)

risposta data 12.04.2017 - 10:57
fonte