Come rifattorizzare il mio disegno, se sembra richiedere più ereditarietà?

6

Recentemente ho fatto una domanda su classi Java che implementano metodi da due fonti (tipo come ereditarietà multipla). Tuttavia, è stato sottolineato che questo tipo di necessità può essere un segno di un difetto di progettazione. Quindi, probabilmente è meglio indirizzare il mio attuale design piuttosto che provare a simulare l'ereditarietà multipla.

Prima di affrontare il problema attuale, alcune informazioni di base su una particolare meccanica in questo framework:

È un semplice framework di sviluppo del gioco. Diversi componenti allocano memoria (come i dati dei pixel), ed è necessario eliminarli non appena non ne hai bisogno. Gli sprite sono un esempio di questo.

Ad ogni modo, ho deciso di implementare qualcosa come Manual-Reference-Counting from Objective-C. Alcune classi, come gli Sprites, contengono un contatore interno, che viene aumentato quando chiami retain() e diminuito su release() .

Così è stata creata la classe astratta Resource . Qualsiasi sottoclasse di questo otterrà gratuitamente le implementazioni retain() e release() . Quando il suo conteggio raggiunge 0 (nessuno sta usando questa classe), chiamerà il metodo destroy() . La sottoclasse deve solo implementare destroy() .

Questo perché non voglio fare affidamento su Garbage Collector per eliminare i dati pixel inutilizzati.

Gli oggetti di gioco sono tutte sottoclassi della classe Node - che è il blocco di costruzione principale, in quanto fornisce informazioni come posizione, dimensione, rotazione, ecc.

Vedi,dueclassisonousatespessonelmiogioco.SpritesandLabels.

Ah...maaspetta.Glispritecontengonodatipixel,ricordi?Ecometale,hannobisognodiestendereResource.

Ma questo, ovviamente, non può essere fatto. Gli sprite sono nodi, quindi devono sottoclasse Node . Ma diamine, sono anche risorse.

  • Perché non creare un'interfaccia Resource?
  • Perché dovrei reimplementare retain() e release() . Sto evitando questo in virtù del fatto che non scrivo più volte lo stesso codice (ricorda che ci sono più classi che hanno bisogno di questo sistema di gestione della memoria).

  • Perché non la composizione?

  • Perché dovrei ancora implementare metodi in Sprite (e classi simili) che chiamano essenzialmente i metodi di Resource . Scriverò ancora lo stesso codice ancora e ancora!

Qual è il tuo consiglio in questa situazione, quindi?

    
posta Omega 31.05.2014 - 06:13
fonte

3 risposte

11

Hai scritto

Sprites contain pixel data

che è un chiaro segno di una relazione "ha una", non una "è una". Quindi morde il proiettile e rendi Sprite - > Risorsa a una composizione.

Why not composition? Because I'd still have to implement methods in Sprite (and similar classes) that essentially call the methods of Resource

Sì, lo farai - ma solo metodi di delega banali. Non dovrai ripetere alcun codice funzionale . Questo è effettivamente accettabile. e il modo standard di composizione / delega funziona. Qui trovi un esempio dettagliato su come sostituire l'ereditarietà per delega. Questo approccio è così comune, c'è persino un refactoring di Eclipse esattamente per questo scopo,

    
risposta data 31.05.2014 - 07:02
fonte
2

Il tuo oggetto Sprite sta effettivamente cercando di assumere 2 responsabilità: gestire la pulizia della memoria e fare il lavoro con i dati dei pixel citati. La lettera S nei principi SOLID consiglia di lasciare ad ogni classe una singola responsabilità. Il pane non si affetta da solo : c'è una forza esterna che prende il pane, lo strumento (i) e fa il suo lavoro. Penserei a quelli che chiameranno questi metodi retain() e release() . I chiamanti vogliono effettivamente "tagliare il pane", cioè "fare il referencecounting dell'oggetto arbitrario"?

Quindi non consiglierei né composito, né ereditare Resource in Sprite , ma rinominarlo in una sorta di ResourceReferenceCounter e usare Sprite e ResourceReferenceCounter indipendentemente. Il modo in cui efficace java garbage collector funziona con gli oggetti dal punto di vista dell'applicazione.

    
risposta data 31.05.2014 - 11:47
fonte
1

tl; dr: la composizione è buona, l'ereditarietà dell'implementazione è ... una scelta sfortunata.

Ciò che provi a raggiungere è far apparire Sprite contemporaneamente come Resource e come Node . Non hai bisogno di questo. Hai bisogno di un modo per esporre l'interfaccia Resource e l'interfaccia Node .

Immaginate:

// traditional part

interface Node {
  Position getPosition();
  // anything else
}

interface Resource {
  void allocate(...);
  void destroy();
}

// composition interfaces

interface NodeHolder {
  Node asNode(); // the only method
}

interface ResourceHolder {
  Resource asResource(); // the only method
}


// reusable resource helper

class ResourceImpl implements Resource {
  public void allocate(...) { /* some implementation */ }
  public void destroy() { /* some implementation */ }
}

// you could have a default Node implementation the same way, 
// but you don't have to; you could directly extend a Node implementation:

class Sprite 
  extends NodeBaseImpl 
  implements NodeHolder, ResourceHolder 
{
  private Resource my_resource;
  public Sprite(...) {
    // whatever construction needed
    my_resource = new ResourceImpl();
  }

  public Resource asResource() { return my_resource; } // composition!
  public Node asNode() { return this; } // composition again! :)
  // ...
} 

Ora tutto il tuo codice di gestione delle risorse funziona su ResourceHolder s e tutto il tuo codice di gestione dei nodi funziona su NodeHolder s. Nessun codice presuppone che qualsiasi oggetto erediti una classe base specifica; vengono utilizzate solo le interfacce:

List<NodeHolder> nodes = new ArrayList<>(5);
nodes.add(new Sprite(...));
nodes.add(new Minimap(...));
nodes.add(new HitpointBar(...));
// can add more NodeHolders
...

// Rotate them all
for (NodeHolder nh : nodes) { nh.asNode().rotate(15); }

Le implementazioni possono essere ereditate o composte, secondo i tuoi gusti. Solo i codici da copiare dappertutto sono i metodi asNode() , ecc., Che di solito sono banali e solo uno per interfaccia.

Raddoppia il numero di interfacce, tuttavia, perché utilizzi nnnHolder dispatch dell'interfaccia per fare esplicitamente ciò che ad es. Go fa automaticamente.

    
risposta data 31.05.2014 - 19:40
fonte

Leggi altre domande sui tag