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.