"Corretto" modo di usare l'ereditarietà

3

All'interno di un piccolo progetto, una classe Storage è pensata per memorizzare qualsiasi tipo di Item . Ora, un Item ha un String name e un'interazione. Ad esempio, un elemento orologio potrebbe implementare l'interazione increment(int num) , mentre un posto potrebbe implementare sit(bool true) .

Devo semplicemente scrivere classi per Item , Clock e Seat e lasciare che Seat e Clock ereditino da Item ? Ovviamente l'interazione che usano varia, quindi è impossibile scrivere tutte le possibili interazioni di Item come metodo virtuale nell'elemento non sono sicuro di come gestire tutti i tipi specifici di interazione.

Al momento, sto trovando il Item e poi usando i condizionali per usare il comportamento corretto sull'interazione, ma sembra una specie di rotatoria e non così eccezionale.

C'è un modo migliore per farlo?

    
posta user188644 25.07.2015 - 21:39
fonte

3 risposte

6

Penso che tu stia soffrendo di YAGNI nel tuo design. Pensaci in questo modo - quando vorresti mai interagire con un orologio e un posto nello stesso modo? Risposta: non lo faresti! Puoi nominare un orologio e nominare un posto, ma non ti siedi su un orologio, né aumenti un posto. Non ha alcun senso.

Le interfacce sono per comportamento comune . Ad esempio, potresti avere un'interfaccia Named , che daresti a tutte le implementazioni con Name . Potresti avere un'interfaccia Timed , che potrebbe essere fornita a tutti Event s che si verificano in un momento specifico. Oppure potresti avere un'interfaccia Sittable , che sarebbe implementata da Chair e Sofa .

Se vuoi davvero avere un'interfaccia Interactable , dovresti o passarla a qualcosa dove l'oggetto stesso può capire cosa sta facendo l'interazione, come Object[] , o l'oggetto di ciò che viene interagito, come Player .

Non pensare a questo tipo di design all'inizio. Scopri che tipo di interazioni stai effettivamente facendo e scopri quale sarà il comportamento comune, quindi progetta le tue interfacce a quel livello.

Risposta al commento:

when one interacts with a room, it uses that object (e.g. a Sofa or a Clock)'s interaction, how can I tell which it is with no prior knowledge?

Questo è quello che stavo dicendo nell'ultimo paragrafo. Metti il comportamento della stanza nel codice per l'oggetto stesso, ad es. (Sintassi Java, ma C # è molto simile)

public class Bed implements Interactable {
  @Override
  void interact(World world, Player player) {
    player.addRest(10);
    world.advanceTime(8, TimeUnit.HOUR);
  }
}
    
risposta data 25.07.2015 - 23:42
fonte
7

Se ti capisco correttamente, sembra che tu stia lavorando con qualcosa come questo pseudocodice:

void foo(Item i) {
   if(i instanceof Clock) {
       (i as Clock).increment();
   } else if(i instanceof Chair) {
       (i as Chair).sit();
   }
}

Come sospetti, questo è un odore di codice. In realtà piuttosto strong.

Il problema immediato è che, poiché questa funzione deve conoscere il tipo concreto di i , non c'è assolutamente alcun vantaggio nell'usare la classe base Item . In effetti, questa funzione sta affermando che può prendere qualsiasi oggetto Item arbitrario, quando non può, quindi sta mentendo a tutti coloro che cercano di usarlo. Se sei molto fortunato, la soluzione è semplice come dividerla in void foo(Clock c) e void foo(Chair c) , ma potrebbe essere più profonda di quella.

Normalmente, questo accade quando si progetta la gerarchia degli oggetti attorno alle cose del mondo reale che si desidera "esistere" nel programma, piuttosto che attorno alle azioni che si desidera effettivamente eseguire il programma . L'unica soluzione semplice è ridisegnare la parte rilevante del tuo programma. Sfortunatamente, senza sapere nulla del resto del tuo programma, possiamo solo indovinare quale sarebbe il cambiamento ottimale. Da un lato, forse puoi trovare una singola interfaccia Interactable che soddisfi le esigenze di tutti i tuoi oggetti e dei loro chiamanti. D'altra parte, forse non ha alcun senso provare a unificare tutti questi tipi disparati e dovresti spostare il codice Chair lontano dal codice Clock . E ci sono molte altre opzioni in mezzo. In ogni caso, sarà difficile capire quale sia il design corretto, ma risolvere il problema ora ti farà risparmiare molto tempo (a meno che questo progetto non rimanga molto piccolo e semplice) .

Nella speranza di essere leggermente più utili di "questo è un problema difficile, ma buona fortuna", proverò a delineare una semplice soluzione di esempio. Il tuo esempio mi ricorda i videogiochi che hanno un pulsante "interagisci", quindi diciamo che sto progettando il codice di interazione dell'articolo per un gioco del genere. Potrei iniziare chiedendomi quali chiavi, munizioni, kit di pronto soccorso, talismani e sacchetti di plastica hanno in comune, ma poi finisco nella situazione che hai descritto.

Invece vorrei chiedere cosa succede quando interagisco con ognuno di questi elementi. Quello che trovo è che, almeno per il gioco specifico che sto giocando oggi , alcune delle possibili risposte includono:

  1. Voglio rimuovere l'elemento dallo schermo, aggiungerlo all'inventario, mostrare un modello 3d di esso insieme a un messaggio "I got X", quindi attendere la pressione di un pulsante.
  2. Voglio mettere in pausa il gioco, mostrare del testo nella parte inferiore dello schermo, attendere che l'utente lo scorra premendo i pulsanti, quindi riprendi.
  3. Come il # 2, ad eccezione dell'ultima porzione di testo che include una scelta, e in base alla selezione dell'utente, possiamo mostrare un'animazione, cambiare lo stato di alcuni altri oggetti nella stanza e generare alcuni mostri prima di riprendere.

Sarebbe molto sensato scrivere un'interfaccia standard per ognuno di questi tipi di elementi, dal momento che tutti gli elementi di un determinato tipo fanno essenzialmente le stesse cose; puoi scrivere una funzione che prende un ShowTextInteractable a cui non importa se è un Book o un CreepyDoll o un Flowerpot (e potrebbe anche non usare la sottoclasse, forse new ShowTextInteractable("It's a flowerpot.") è abbastanza buono). Notare anche che non ho nemmeno toccato l'uso di oggetti dall'inventario; quelli dovrebbero essere tipi completamente diversi dagli oggetti "nel mondo" perché fanno cose completamente diverse. Ma spero che ti dia un'idea di cosa sto parlando.

    
risposta data 25.07.2015 - 22:43
fonte
2

Un altro approccio sarebbe usare la composizione invece dell'ereditarietà. Possiamo dare uno sguardo al schema di comando per l'ispirazione. Vorremmo avere un'interfaccia comune mentre consentiamo azioni variabili. Piuttosto che scrivere il comportamento in Item stesso (come fa durron597 ), potremmo invece creare un oggetto intermedio per mantenere il comportamento interattivo che vogliamo. Iniziamo con l'interfaccia:

interface Interaction {
    void interact(/** things to pass in **/);
}

Ora creiamo un'implementazione:

class ClockInteraction implements Interaction {
    private Clock clock;

    public ClockInteraction(Clock clock) {
        this.clock = clock;
    }

    public void interact() {
        clock.increment(1);
    }
}

E un altro:

class SeatInteraction implements Interaction {
    private Seat seat;
    private boolean shouldSit;

    public SeatInteraction(Seat seat, boolean shouldSit) {
        this.seat = seat;
        this.shouldSit = shouldSit;
    }

    public void interact() {
        seat.sit(shouldSit);
    }
}

Il comportamento di Interaction è ora separato dal comportamento di Item . Questo ci fornisce vantaggi oltre all'implementazione del comportamento direttamente in Item s? Innanzitutto, possiamo creare più comportamenti per lo stesso tipo di oggetto:

class ClockReverseInteraction implements Interaction {
    private Clock clock;

    public ClockReverseInteraction(Clock clock) {
        this.clock = clock;
    }

    public void interact() {
        clock.increment(-1);
    }
}

Ora ho due comportamenti diversi che possono essere usati sullo stesso oggetto, e posso scegliere tra loro in fase di runtime.

Possiamo anche comporre più Item s in un singolo comportamento:

class SitWhenInteraction implements Interaction {
    private Clock clock;
    private Seat seat;

    public SitWhenInteraction(Clock clock, Seat seat) {
        this.clock = clock;
        this.seat = seat;
    }

    public void interact() {
        seat.sit(clock.value() < 10);
    }
}

Ora, anziché Storage trattiene Item s, dovrebbe invece contenere Interaction s:

foreach(Interaction interaction : storage) {
    interaction.interact();
}
    
risposta data 29.07.2015 - 07:28
fonte

Leggi altre domande sui tag