Skip to content

Latest commit

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Part One: Get Your Game On

The player has an inventory of weapons and potions. These can affect a player's health. For debugging purposes, we can also fetch a summary of a player's inventory.

publicclassPotion {
privatefinalStringname;
privatefinalinthealAmount;
publicPotion(Stringname, inthealAmount) {
this.name = name;
this.healAmount = healAmount;
}
publicStringgetName() {
returnname;
}
publicintgetHealAmount() {
returnhealAmount;
}
publicvoidquaff(Playertarget) {
target.modifyHealth(healAmount);
}
}
publicclassWeapon {
privatefinalStringname;
privatefinalintattackPower;
publicWeapon(Stringname, intattackPower) {
this.name = name;
this.attackPower = attackPower;
}
publicStringgetName() {
returnname;
}
publicintgetAttackPower() {
returnattackPower;
}
publicvoidattack(Playertarget) {
target.modifyHealth(-attackPower);
}
}
publicclassPlayer {
privatefinalStringname;
privateinthealth = 100;
privatefinalList<Weapon> weapons = newArrayList<>();
privatefinalList<Potion> potions = newArrayList<>();
publicPlayer(Stringname) {
this.name = name;
}
publicStringgetName() {
returnname;
}
publicintgetHealth() {
returnhealth;
}
publicvoidaddWeapon(Weaponweapon) {
weapons.add(weapon);
}
publicvoidaddPotion(Potionpotion) {
potions.add(potion);
}
publicvoidmodifyHealth(intamount) {
health += amount;
if (health > 100) {
health = 100;
}
if (health < 0) {
health = 0;
}
}
publicList<String> describeInventory() {
List<String> result = newArrayList<>();
for (Weaponweapon : weapons) {
result.add(String.format(
"%s - %d ATK", weapon.getName(), weapon.getAttackPower()
));
}
for (Potionpotion : potions) {
result.add(String.format(
"%s - heals %d", potion.getName(), potion.getHealAmount()
));
}
returnresult;
}
}
publicclassGame {
publicstaticvoidmain(String[] args) {
Playerarthur = newPlayer("Arthur Pendragon");
Weaponexcalibur = newWeapon("Excalibur", 25);
Potionminor = newPotion("Minor healing draught", 15);
Potionsenzu = newPotion("Senzu bean", 100);
arthur.addWeapon(excalibur);
arthur.addPotion(minor);
arthur.addPotion(senzu);
Playercuchulainn = newPlayer("Cú Chulainn");
Weapongaebulg = newWeapon("Gáe Bulg", 30);
cuchulainn.addWeapon(gaebulg);
gaebulg.attack(arthur);
minor.quaff(arthur);
System.out.println("Arthur health: " + arthur.getHealth());
for (Stringdescriptor : arthur.describeInventory()) {
System.out.println(descriptor);
}
}
}

Run this and the result is:

Arthur health: 85
Excalibur - 25 ATK
Minor healing draught - heals 15
Senzu bean - heals 100

Right now, this design is okay. It could be improved, but nothing is screaming out for abstraction: a lot of developers will follow the "rule of three" where if you repeat yourself twice, that is acceptable, but if you repeat yourself three times, you should abstract.

Part Two: We Could All Use a Little Change

Our game designers have decided that players need bags to handle inventory management. Bags can contain weapons, potions, and even other bags. They also have a finite capacity.

So we add a Bag class:

publicclassBag {
privatefinalStringname;
privatefinalintcapacity;
privateintcount = 0;
privatefinalList<Weapon> weapons = newArrayList<>();
privatefinalList<Potion> potions = newArrayList<>();
privatefinalList<Bag> bags = newArrayList<>();
publicBag(Stringname, intcapacity) {
this.name = name;
this.capacity = capacity;
}
publicStringgetName() {
returnname;
}
publicintgetCapacity() {
returncapacity;
}
publicintgetCount() {
returncount;
}
/* * These getters return a read-only copy of the list to enforce that you * must use the "add" methods below. Ensures nobody can push us past our * capacity. */publicList<Weapon> getWeapons() {
returnCollections.unmodifiableList(weapons);
}
publicList<Potion> getPotions() {
returnCollections.unmodifiableList(potions);
}
publicList<Bag> getBags() {
returnCollections.unmodifiableList(bags);
}
publicvoidincrementCount() {
if (count == capacity) {
thrownewRuntimeException("Already at capacity");
}
count++;
}
publicvoidaddWeapon(Weaponweapon) {
incrementCount();
weapons.add(weapon);
}
publicvoidaddPotion(Potionpotion) {
incrementCount();
potions.add(potion);
}
publicvoidaddBag(Bagbag) {
incrementCount();
bags.add(bag);
}
}

In our Player class, we make some modifications, including making describeInventory into a recursive call that descends through all the player's bags and shows their contents, indented.

publicclassPlayer {
// ...privatefinalList<Bag> bags = newArrayList<>();
publicvoidaddBag(Bagbag) {
bags.add(bag);
}
publicList<String> describeInventory() {
returndescribeInventoryInternal("", weapons, potions, bags);
}
privatestaticList<String> describeInventoryInternal(
Stringindent,
List<Weapon> weapons,
List<Potion> potions,
List<Bag> bags
) {
List<String> result = newArrayList<>();
for (Weaponweapon : weapons) {
result.add(String.format(
"%s%s - %d ATK", indent, weapon.getName(), weapon.getAttackPower()
));
}
for (Potionpotion : potions) {
result.add(String.format(
"%s%s - heals %d", indent, potion.getName(), potion.getHealAmount()
));
}
for (Bagbag : bags) {
result.add(String.format(
"%s%s - (%d/%d)", indent, bag.getName(), bag.getCount(), bag.getCapacity()
));
result.addAll(
describeInventoryInternal(
indent + " ",
bag.getWeapons(),
bag.getPotions(),
bag.getBags()
)
);
}
returnresult;
}
}

Our Game class can now look like:

publicclassGame {
publicstaticvoidmain(String[] args) {
Playerarthur = newPlayer("Arthur Pendragon");
Weaponexcalibur = newWeapon("Excalibur", 25);
arthur.addWeapon(excalibur);
Playercuchulainn = newPlayer("Cú Chulainn");
Weapongaebulg = newWeapon("Gáe Bulg", 30);
BagninjaBag = newBag("Ninja's bag", 5);
BagkunaiBundle = newBag("Kunai bundle", 20);
for (inti = 0; i < 5; i++) {
kunaiBundle.addWeapon(newWeapon("Kunai", 5));
}
ninjaBag.addBag(kunaiBundle);
Potionpoison = newPotion("Nightshade poison", -100);
ninjaBag.addPotion(poison);
cuchulainn.addWeapon(gaebulg);
cuchulainn.addBag(ninjaBag);
excalibur.attack(cuchulainn);
poison.quaff(cuchulainn);
System.out.println("Cú Chulainn health: " + cuchulainn.getHealth());
for (Stringdescriptor : cuchulainn.describeInventory()) {
System.out.println(descriptor);
}
}
}

And when we run it:

Cú Chulainn health: 0
Gáe Bulg - 30 ATK
Ninja's bag - (2/5)
Nightshade poison - heals -100
Kunai bundle - (5/20)
Kunai - 5 ATK
Kunai - 5 ATK
Kunai - 5 ATK
Kunai - 5 ATK
Kunai - 5 ATK

Part Three: They Don't Stop Coming

We're starting to see some real maintenance nightmares on the horizon now. We listened in on a conversation with the game designers, and we're hearing that there are several changes to be proposed soon:

  • Bag capacity should be based on weight, not just item count. Which means that everything that can be in a bag will need weight to be tracked.
  • Everything in a player's inventory should also have a color that the player can set to organize things at a glance.
  • They want to add armor that can reduce damage from attacks. Also wands that can cast spells.

Now we're starting to repeat ourselves everywhere. We will need to add classes for Armor and Wand, but we also need to modify code in Player and Bag to handle them. Armor, Wand, Bag, Weapon, and Potion will all need code to handle weight and color.

So before those requests even come in, we're going to refactor our code, abstract out some of the shared logic.

First, we introduce a new Item abstract class as the superclass of things that can be in an inventory:

publicabstractclassItem {
privatefinalStringname;
publicItem(Stringname) {
this.name = name;
}
publicStringgetName() {
returnname;
}
}

This is the class that will eventually handle even more shared concerns like weight and color.

Then we modify Weapon, Potion, and Bag to extend that class. Now, anywhere that we had a List<Weapon>, List<Potion>, List<Bag>... we can just have a List<Item>.

So now our Item subclasses look like:

publicclassPotionextendsItem {
privatefinalinthealAmount;
publicPotion(Stringname, inthealAmount) {
super(name);
this.healAmount = healAmount;
}
publicintgetHealAmount() {
returnhealAmount;
}
publicvoidquaff(Playertarget) {
target.modifyHealth(healAmount);
}
}
publicclassWeaponextendsItem {
privatefinalintattackPower;
publicWeapon(Stringname, intattackPower) {
super(name);
this.attackPower = attackPower;
}
publicintgetAttackPower() {
returnattackPower;
}
publicvoidattack(Playertarget) {
target.modifyHealth(-attackPower);
}
}
publicclassBagextendsItem {
privatefinalintcapacity;
privateintcount = 0;
privatefinalList<Item> contents = newArrayList<>();
publicBag(Stringname, intcapacity) {
super(name);
this.capacity = capacity;
}
publicintgetCapacity() {
returncapacity;
}
publicintgetCount() {
returncount;
}
/* * This getter returns a read-only copy of the list to enforce that you * must use the "add" method below. Ensures nobody can push us past our * capacity. */publicList<Item> getContents() {
returnCollections.unmodifiableList(contents);
}
publicvoidaddItem(Itemitem) {
if (count == capacity) {
thrownewRuntimeException("Already at capacity");
}
count++;
contents.add(item);
}
}

These are already a lot simpler and less repetitive. Finally, we modify Player:

publicclassPlayer {
privatefinalList<Item> inventory = newArrayList<>();
publicvoidaddItem(Itemitem) {
inventory.add(item);
}
publicList<String> describeInventory() {
returndescribeInventoryInternal("", inventory);
}
privatestaticList<String> describeInventoryInternal(Stringindent, List<Item> items) {
List<String> result = newArrayList<>();
for (Itemitem : items) {
if (iteminstanceofWeapon) {
Weaponweapon = (Weapon) item;
result.add(String.format(
"%s%s - %d ATK", indent, weapon.getName(), weapon.getAttackPower()
));
}
elseif (iteminstanceofPotion) {
Potionpotion = (Potion) item;
result.add(String.format(
"%s%s - heals %d", indent, potion.getName(), potion.getHealAmount()
));
}
elseif (iteminstanceofBag) {
Bagbag = (Bag) item;
result.add(String.format(
"%s%s - (%d/%d)", indent, bag.getName(), bag.getCount(), bag.getCapacity()
));
result.addAll(
describeInventoryInternal(indent + " ", bag.getContents())
);
}
}
returnresult;
}
}

So, now because we're using a polymorphic typeItem, instead of having multiple collections to deal with different kinds of items, we deal with them all together. Run the Game class and we see the only noticeable change is the order of the inventory report, because now we show everything in the order it was added, rather than implicitly grouping by type. But that's okay: we had no requirements on the ordering of this information.

Cú Chulainn health: 0
Gáe Bulg - 30 ATK
Ninja's bag - (2/5)
Kunai bundle - (5/20)
Kunai - 5 ATK
Kunai - 5 ATK
Kunai - 5 ATK
Kunai - 5 ATK
Kunai - 5 ATK
Nightshade poison - heals -100

There's one more improvement we can do though.

Part Four: You Hit the Ground Running

Now if the designers want to add weight or color, we're set: we only have to add those in one place. And if they want to add armor or wands, we've made things better, because we don't need to add new collections for those types and we don't need to modify the Bag class at all. But we still have to modify Player because describeInventoryInternal is still aware of each of the item types.

This is a pretty good example of a "code smell": any time you see conditional behavior that's based on types, it's usually something that can be improved with polymorphism.

In this case, describeInventoryInternal is going through each item and, based on its type, adding one to several strings to a list. Instead of using a conditional like this, we can pull this functionality into the classes themselves by using an abstract method on Item:

publicabstractclassItem {
privatefinalStringname;
publicItem(Stringname) {
this.name = name;
}
publicStringgetName() {
returnname;
}
publicabstractList<String> getDescriptors(Stringindent);
}

And then we just need to implement that method on each subclass:

publicclassPotionextendsItem {
@OverridepublicList<String> getDescriptors(Stringindent) {
returnCollections.singletonList(
String.format("%s%s - heals %d", indent, getName(), healAmount)
);
}
}
publicclassWeaponextendsItem {
@OverridepublicList<String> getDescriptors(Stringindent) {
returnCollections.singletonList(
String.format("%s%s - %d ATK", indent, getName(), attackPower)
);
}
}
publicclassBagextendsItem {
@OverridepublicList<String> getDescriptors(Stringindent) {
List<String> result = newArrayList<>();
result.add(String.format("%s%s - (%d/%d)", indent, getName(), count, capacity));
for (Itemitem : contents) {
result.addAll(item.getDescriptors(indent + " "));
}
returnresult;
}
}

And modify Player to use it:

publicclassPlayer {
publicList<String> describeInventory() {
List<String> result = newArrayList<>();
for (Itemitem : inventory) {
result.addAll(item.getDescriptors(""));
}
returnresult;
}
}

Now, when the designers want to add Armor or Wand, we only need to add classes for those types, and we don't need to touch anything in Player or Bag to allow them to function correctly in the inventory system.

Now, there's still some repetition in getDescriptors. Every subclass includes %s%s - at the start of its format string and fills in indent and getName() to those fields. So I considered pulling this functionality into Item – but because of Bag emitting multiple lines, the complexity introduced by this abstraction was unlikely to be worth it.

Conclusion: You're Bundled Up Now

So what was the point of the tutorial being so long and going through each of these steps, why not just show the final code in step 4?

Well, that's what makes something a tutorial and not just reference documentation. If you just need to know how to declare class inheritance in a language, just what it looks like, you can look up the language reference for that.

The point of going through each of these steps is to understand:

  1. What problem is this abstraction going to solve?
  2. What are the benefits of using this abstraction?
  3. What are the drawbacks of it?

OOP is, at the most basic, a set of techniques for software abstraction. These are the skills required to be effective at using it.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages