Una decisione di progettazione in composizione o aggregazione

4

Recentemente ho avuto dei dubbi su alcune decisioni di progettazione che faccio spesso, quindi questa volta ho deciso di prendermi del tempo e provare ad analizzarlo per trovare la soluzione migliore. Descriverò uno scenario semplice in cui la decisione di progettazione può essere facilmente compresa. :)

Supponiamo che stiamo sviluppando un gioco in Java in cui i giocatori possono avere un numero qualsiasi di diversi tipi di oggetti.

/** Class that represent an item type existing in the game */
// you may prefer to call this ItemType,
// but for me appending the word "Type" is not necessary
class Item {
    private String descrip;
    private Image icon;
}

In questa situazione ho bisogno di tenere traccia del numero di ogni oggetto che il giocatore ha nella sua borsa, quindi tengo una collezione di essi, in ogni elemento di cui ho tipo e quantità. Inoltre si può pensare ad altri attributi che appartengono a quella relazione (forse una capacità massima), ma la domanda è: come modellare questo scenario in generale, ma con un'implementazione pratica in Java? Ecco due approcci che ho trovato:

Uso di un elenco

class Slot {
    private Item item;
    private int quantity;

    /** Auto generated */
    public int hashCode()
    {
        int hash = 7;
        hash = 13 * hash + (this.item != null ? this.item.hashCode() : 0);
        return hash;
    }

    /** This method is also auto generated and just compares the Item, 
        independent of quantity */
    public boolean equals(Object object) 
    {
        if (!(object instanceof Slot))
        {
            return false;
        }
        Slot other = (Slot) object;
        if ((this.item == null && other.item != null) 
             || (this.item != null && !this.item.equals(other.item)))
        {
            return false;
        }

        return true;
    }
}

La classe precedente riporta un articolo con una quantità e un insieme di Slot s è l'attributo principale della classe Inventory . Gli svantaggi di ciò vengono visualizzati non appena disegno l'interfaccia Inventory per non esporre Slot al client (rendendo Slot privato o nascondendolo in qualche altro modo): creando un'interfaccia semplice ma fornendo accesso a tutti gli standard metodi di raccolta. A mio modesto parere non c'è niente di sbagliato in questo:

/** Class that represent the bag that holds the player's items */
class Inventory {
   // this collection holds only one element for each type of item
   private ArrayList<Slot> slots; 

    public Inventory()
    {
        slots = new ArrayList<Slot>(10);
    }

    public void add(Item item, int quantity)
    {
        Slot slot = new Slot(item, quantity);
        // if we don't already have a Slot for the added Item, add it
        if (!slots.contains(slot))
        {
            slots.addElement(slot);
        }
        // if we already have a Slot for the added Item, increase the quantity
        else
        {
            Slot _slot = slots.elementAt(slots.indexOf(slot));
            _slot.addQuantity(quantity);
        }
    }

    public void subtract(Item item, int quantity)
    {
        Slot slot = new Slot(item, 0); 
        int index = slots.indexOf(slot);

        // only if we actually have this item
        if (index != -1) 
        {
            Slot _slot = (Slot) slots.elementAt(index);
            _slot.quantity -= quantity;

            // if we have fewer of the Item than the number to remove, 
            // remove the slot from the collection
            if (_slot.quantity <= 0)
            {
                slots.removeElementAt(index);
            }
        }
    }

    public void remove(Item item)
    {
        Slot slot = new Slot(item,0);
        slots.removeElement(slot);
    }

    public boolean hasItem(Item item)
    {
        Slot slot = new Slot(item,0);
        return slots.contains(slot);
    }
}

Come vedi, dobbiamo passare attraverso alcuni anelli per implementare alcuni metodi di raccolta: in hasItem dobbiamo creare un elemento Slot solo per quell'operazione. Nei metodi aggiungi e sottrazione, dobbiamo creare l'oggetto Slot solo per verificare se è lì. Mi chiedo se sia davvero necessario. Suppongo che lo sia se desideriamo fornire un'interfaccia ai metodi di raccolta standard. Forse possiamo spostare tutta questa logica in un'altra classe (ad esempio: Slots ), esponendo un'interfaccia più pulita che ci consente di cercare per elemento senza la necessità di creare una Slot, ma stiamo semplicemente spostando lo stesso problema in un'altra classe. Affronteremo lo stesso problema, oltre al fatto che Inventory è fondamentalmente un insieme di Slot s, stiamo solo creando un'altra classe per essere esattamente la stessa cosa, il che non ha molto senso me.

Sarebbe più corretto cambiare l'interfaccia di questa classe per lavorare direttamente con la classe Slot ? Ciò implica lo spostamento dalla composizione (con Slot s creata all'interno di Inventory) a un'aggregazione (con creazione Slot all'esterno di Inventory). Il risultato potrebbe essere simile a:

/** Class that represent the bag that holds the player's items */
class Inventory {
   private ArrayList<Slot> slots;

    public Inventory()
    {
        slots = new ArrayList<Slot>(10);
    }

    public void add(Slot slot)
    {
        if (!slots.contains(slot))
        {
            slots.addElement(slot);
        }
        else
        {
            Slot _slot = slots.elementAt(slots.indexOf(slot));
            _slot.addQuantity(slot.getQuantity());
        }
    }

    public void subtract(Slot slot, int quantity) // how would the client get the Slot?
    {
        int index = slots.indexOf(slot);

        // only if we actually have this item
        if (index != -1) 
        {
            Slot _slot = (Slot) slots.elementAt(index);
            _slot.quantity -= quantity;

            // if we have fewer of the Item than the number to remove, 
            // remove the slot from the collection
            if (_slot.quantity <= 0)
            {
                slots.removeElementAt(index);
            }
        }
    }

    public void remove(Slot slot) // again, how would the client get the Slot?
    {
        slots.removeElement(slot);
    }

    // Hard to change a parameter type here. This strongly suggest there is
    // a flaw in the design. I only keep this here to communicate the observation
    public boolean hasItem(Item item) 
    {
        Slot slot = new Slot(item, 0); // here we have no choice but to create it
        return slots.contains(slot);
    }
}

Ma di nuovo, è conveniente? Non metto spesso in pratica questo approccio, quindi non ne sono sicuro.

Utilizzo di una ricerca

Un altro approccio consiste nell'usare qualcosa come un Hashtable con oggetti Item come quantità di chiavi come valori, quindi solo la quantità è associata a Articoli, senza la necessità di una classe Slot . Un'implementazione Inventory utilizzando Hashtable potrebbe essere simile a:

public class Inventory
{
    private Hashtable items;

    public Inventory()
    {
        items = new Hashtable(10);
    }

    public void add(Item item, int amount)
    {
        int stock = amount;
        if (items.containsKey(item))
        {
            stock += ((Integer) items.get(item)).intValue();
        }
        items.put(item, new Integer(stock));
    }

    public void subtract(Item item, int amount)
    {
        if (items.containsKey(item))
        {
            int stock = ((Integer) items.get(item)).intValue();
            stock -= amount;
            if (stock > 0)
            {
                items.put(item, new Integer(stock));
            }
            else
            {
                items.remove(item);
            }
        }
    }

    public void remove(Item item)
    {
        this.items.remove(item);
    }

    public boolean hasItem(Item item)
    {
        return this.items.containsKey(item);
    }
}

Per me sembra molto meglio che usare un elenco perché non c'è bisogno della classe Slot e tutte le interazioni con la raccolta Item sono molto più semplici, ma non sono sicuro se ho ragione.

Lo stesso tipo di problema è evidente anche in questo scenario: Supponiamo che stiamo implementando un'applicazione di ordinazione per una pizzeria; ogni ordine è costituito da tipi di pizze e quantità.

/** Type of pizza */
class Pizza {
    private String descrip;
    private Image icon;
}

/** Customer request for pizza */
class Request {
    // for each type of pizza we have to keep a count of how many the customer ordered
}

/** A pizza order */
class Order {
    private Pizza pizza;
    private int   amount;
    privete BigDecimal price;
}

Sapendo come risolvere il primo problema, ho uno schema da usare per questa situazione simile; quindi la domanda è, quale soluzione è la migliore?

Cerco di convincermi che non sono l'unica persona con questa preoccupazione, che le persone comuni come me affrontano questo problema regolarmente, quindi spero di trovare in che modo le altre persone risolvono questo problema. D'altra parte, so che questa domanda è abbastanza grande e forse un po 'elaborata, quindi forse ho bisogno di dividerlo in diverse domande più specifiche. Forse la risposta a questo problema è semplice, sto complicando troppo le cose, o peggio ancora, i miei dubbi su come gestire questa situazione sorgono perché non capisco davvero alcuni principi di base del progetto, come un capitano su una nave che usa un cannocchiale per vedere terra ma non si rende conto che la sua nave è stata arenata su un terreno solido che era vicino nel momento in cui ha disegnato il suo cannocchiale per guardare. Va bene, abbastanza poesia a buon mercato (ma era difficile descrivere i miei sentimenti senza di lui hehehe).

Mi scuso per la lunghezza della domanda e apprezzerei molto l'aiuto di qualcuno con questo problema.

    
posta Victor 08.04.2013 - 06:57
fonte

2 risposte

2

C'è molto da dire qui.

  1. L'API di Inventory ha l'aspetto di un oggetto valore (poiché non ha identità, da solo, suppongo che appartenga a un Player ). Se è così, dovrebbe essere immutabile, restituendo una nuova istanza ogni volta che aggiungi o rimuovi un Item .
  2. La tua classe Item ha l'aspetto di un identificatore condiviso , quindi dovrebbe sostituire equals e hashcode e (in senso stretto, in termini DDD) non dovrebbe contenere alcun riferimento al icon poiché non è utile per i propri invarianti. dovresti utilizzare un servizio di dominio che prende l'elemento e fornisce icona corretta invece (probabilmente avvolgendo una Hashtable definita staticamente). Tuttavia, sii pragmatico con questo: se tu misura i problemi di rendimento effettivo con tale approccio (formalmente corretto), mantieni il campo lì.
  3. Il Slot , così com'è, non può essere considerato un oggetto dominio, perché non implementa l'uguaglianza in un modo rilevante del dominio (ignora la quantità). Questo è perfettamente corretto per una classe privata utilizzata internamente da Inventory , ma non deve essere esposta ai client (come nell'implementazione del secondo Inventory della prima soluzione che hai proposto). Questo perché i client possono confrontare per l'uguaglianza due Slot s da due giocatori diversi, ottenendo risultati inaffidabili.
  4. Per sbarazzarmi della maggior parte del codice standard, farei il codice
    • un'interfaccia functor di supporto come ItemQuantityChanger (sotto)
    • una raccolta "funzionale" immutabile, ad esempio ItemsSet , con un metodo di firma ItemsSet set(Item item, ItemQuantityChanger transformation) che scorre su un semplice array per la modifica dell'oggetto. Tale metodo creerebbe sempre un nuovo ItemsSet da restituire, senza l'Item (se la trasformazione restituisce zero), con un nuovo Item se l'elemento non viene trovato (con la quantità restituita applicando la trasformazione a zero) o semplicemente con l'elemento precedente accoppiato con il risultato della trasformazione.

Tuttavia, in concreto, vorrei solo usare un ImmutableList.Builder , dal momento che il design funzionale descritto in 4 aggiunge poco valore in questo caso. Un singolo se , in un metodo, non è quell'odore enorme.

public interface ItemQuantityChanger {
   /**
    * Accept the current quantity of an Item and return the new one.
    */
   int applyTo(int currentQuantity);
}
    
risposta data 08.04.2013 - 12:28
fonte
1

Per quanto posso dire, la tua domanda si riduce a (nel contesto del tuo esempio):

  1. Che tipo di interfaccia dovrebbe avere Inventory ?

  2. In che modo Inventory tiene traccia della sua Item raccolta?

Che tipo di interfaccia dovrebbe avere Inventory?

Lo spazio pubblicitario è un'astrazione che semplifica il modo in cui la classe Player gestisce i suoi elementi, pertanto l'interfaccia deve essere definita in modo che Player non sappia nulla sull'implementazione di tale raccolta. Ciò ti consente di cambiare completamente il modo in cui Inventory tiene traccia degli elementi, forse vuoi iniziare a tracciare la capacità totale di Item peso invece di dimensione, ecc. Senza richiedere alcuna modifica nel codice Player . Oltre a ciò, la gestione della raccolta di Item s è responsabilità di Inventory , quindi la logica per farlo non appartiene a nessun altro.

Solo una nota: la capacità massima non sembra appartenere a dove l'hai descritta. Idealmente il tuo Inventory avrebbe una capacità massima, diciamo 100, e diversi Item s contribuirebbero a un determinato valore per quella capacità; per esempio, una pozione di salute dovrebbe contribuire a 5 unità, un paio di scarponi contribuirebbe a 20 unità, ecc.

In che modo Inventory tiene traccia della sua raccolta articoli?

È preferibile un List se si desidera conservare più copie dello stesso elemento e, poiché in questo caso si desidera solo elementi distinti, utilizzare una ricerca. Hashtable è sincronizzato, ma in modo ingenuo (vedi uno dei commenti a questa domanda SO ), quindi HashMap probabilmente servirebbe meglio. Ovviamente, poiché dovremmo sempre programmare interfacce anziché implementazioni, sarebbe:

Map<Item, Integer> items = new HashMap<Item, Integer>();

Un'altra nota: in genere non è necessario specificare la capacità di raccolta iniziale a meno che non si stia ottimizzando e, in ogni caso, la capacità iniziale di Hashtable è 11 e HashMap è 16.

Per comprendere meglio i principi di progettazione, consulta i principi SOLID , e se vuoi approfondire tutti gli aspetti dello sviluppo del software, acquistare una copia di codice completo e leggerla . : -)

    
risposta data 10.04.2013 - 18:44
fonte

Leggi altre domande sui tag