I've recently had doubts about some design decisions that I frequently make, so this time I decided to take some time and try to analyze it to find the best solution. I will describe a simple scenario where the design decision can be easily understood. :)
Suppose we are developing a game in Java where players can have any number of many different types of items.
/** 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 this situation I need to keep track of how many of each item the player has in his bag, so I keep a collection of them, in each element of which I have item type and quantity. Additionally one can think of other attributes that belong to that relation (perhaps a max capacity), but the question is: how do I model this scenario in general but with a practical implementation in Java? Here are two approaches I've come up with:
Using a List
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;
}
}
The above class relates one item with a quantity, and a collection of Slot
s is the primary attribute of the Inventory
class. The disadvantages of this appear as soon I design the Inventory
interface to not expose Slot
to the client (making Slot
private or hiding it in some other way): in creating a simple interface but providing access to all the standard collection methods. In my humble opinion there is nothing wrong with this:
/** 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);
}
}
As you see, we have to jump through some hoops to implement certain collection methods: in hasItem
we have to create a Slot
element just for that operation. In the add and subtract methods, we have to create the Slot
object just to check if it's there.
I wonder if this really necessary. I suppose that it is if we desire to provide an interface to the standard collection methods. Perhaps we can move all this logic to another class (for example: Slots
), exposing a cleaner interface that lets us search by item without the need of creating a Slot, but we are just moving the same problem to another class. We will face the same problem there, besides the fact that Inventory
is basically a set of Slot
s, we're really just creating another class to be exactly the same thing, which doesn't make a lot of sense to me.
Would it be more correct to change the interface of this class to work directly with Slot
class instead? This implies moving from composition (having Slot
s created within Inventory) to an aggregation (having Slot
creation outside of Inventory). The result might look like:
/** 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);
}
}
But again, is this convenient? I don't often put this approach into practice, so I'm not sure.
Using a lookup
Another approach is to use something like a Hashtable with Item
objects as keys quantities as values, so only quantity is associated with Items, without the need for a Slot
class. An Inventory
implementation using Hashtable might look like:
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);
}
}
To me this seems much better than using a List because there is no need of the Slot
class, and all interactions with the Item
collection are much simpler, but I'm not sure if I'm right.
The same type of problem is also evident in this scenario: Suppose that we are implementing an order application for a pizza shop; every order consists of types of pizzas and quantities.
/** 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;
}
Knowing how to solve the first problem, I have a pattern to use for this similar situation; so the question is, what solution is best?
I try to convince myself that I'm not the only person with this concern, that ordinary people like me face this problem on a regular basis, so I hope to find how other people solve this problem. On the other hand, I know that this question is quite big and maybe a little elaborate, so maybe I need to split it into several more specific questions. Maybe the answer to this problem is simple I'm over-complicating things, or even worse, my doubts about how to handle this situation arise because I really don't understand some basic design principle, like a captain on a ship using a spyglass in order to sight land but doesn't realize that his ship has been beached on solid ground that was close at the moment he draw his spyglass to look. Okay, enough cheap poetry (but it was hard to describe my feelings without it hehehe).
I apologize for the length of the question, and would very much appreciate anyone's help with this problem.