Kotlin 83 - Item Type?
We’d like a better way of getting from an items name to its type. We’d like it to be bullet-proof, but not difficult to manage.
Yesterday, we introduced the notion of product “type”, with enumeration of the possible types:
enum class TYPE {
AGELES, CHEESE, CONJUR, PASSES, NORMAL
}
That allows us to have a dispatch on type such that the compiler will give us an error if we were to introduce a new kind of product type. The when
knows the elements of the enum, and requires that they all be present (or that an else
is provided):
private fun Item.updateQuality() {
when (type()) {
TYPE.AGELES -> { /*nothing*/ }
TYPE.CHEESE -> raiseQuality(1)
TYPE.CONJUR -> lowerQuality(2)
TYPE.PASSES -> adjustPassQuality()
TYPE.NORMAL -> adjustNormalQuality()
}
}
If we were to introduce a new product type, perhaps TYPE.DISCOUNT
, but we forgot to put a line in the when, the program wouldn’t compile and it would tell us why. So that’s nice.
However, we do not have a very robust way of knowing the type of an item, given its name. In a real database-driven system, we’d probably have a table mapping from product name to type (and perhaps other general ideas relating to type). I have no plans whatsoever to put database access into Gilded Rose, but we can address some of the same issues.
Here’s how we get the TYPE
now:
private fun Item.type(): TYPE {
return when (name) {
PROD_BRIE -> TYPE.CHEESE
PROD_CONJ -> TYPE.CONJUR
PROD_ELIX -> TYPE.NORMAL
PROD_PASS -> TYPE.PASSES
PROD_SULF -> TYPE.AGELES
PROD_VEST -> TYPE.NORMAL
else -> {
println("Product $name is not found up in type(), NORMAL assumed")
TYPE.NORMAL }
}
}
Now this mapping is from specific product name to type. Those PROD_SULF
things are constants representing the specific name of specific items:
const val PROD_BRIE = "Aged Brie"
const val PROD_CONJ = "Conjured Mana Cake"
const val PROD_ELIX = "Elixir of the Mongoose"
const val PROD_PASS = "Backstage passes to a TAFKAL80ETC concert"
const val PROD_SULF = "Sulfuras, Hand of Ragnaros"
const val PROD_VEST = "+5 Dexterity Vest"
Yesterday, I discovered a mistake that I had made. I had tested a “conjured” item, but its name was not “Conjured Mana Cake”. The result was that my item worked correctly, but that the mana cake did not, because it was not found in the type() list above. Similarly, if we ever get backstage passes to a Beyoncé concert, they’re not going to be priced like passes until we update that method.
I’d at least like to get all the information physically together, so that the string constants and type constants and whatever we use for patterns and such would all be together, where we can find everything we need and thus have a chance of getting the edits right.
One rather common way of doing that is to implement a class. As I think about this, I’m thinking that at Gilded Rose we call things “items”, not “products”. Probably my strings PROD_VEST
and such should be called ITEM_VEST
. Let’s make that change. It’ll warm up our fingers and should also bring ideas closer together.
I’ll just repeatedly use IDEA’s rename for this.
const val ITEM_BRIE = "Aged Brie"
const val ITEM_CONJ = "Conjured Mana Cake"
const val ITEM_ELIX = "Elixir of the Mongoose"
const val ITEM_PASS = "Backstage passes to a TAFKAL80ETC concert"
const val ITEM_SULF = "Sulfuras, Hand of Ragnaros"
const val ITEM_VEST = "+5 Dexterity Vest"
Test, green. Commit: Rename PROD_ to ITEM_.
I was hoping that doing that would give me a good idea. I’m not at all sure that it has.
Let’s imagine what we need. Certainly we want to be able to say, for any item, exactly what TYPE it is. I think we would also like to provide simple patterns, perhaps substrings, that imply a given TYPE. “Conjured” comes to mind, because we already need it.
We’d like to be sure that every defined ITEM gets a specific type, that is, it doesn’t fall into the default case. (Right now we treat them as NORMAL. That’s fine, but we should probably print something in the output as well, so that when the listing of prices comes out, it shows items that aren’t set up right).
I think that the Kotlin enum may be able to help us here. Each element of an enum, I’m told, is an instance of the enum class. It can have behavior and associated values.
I’m going to type in an enum as an experiment: I’m not yet expert in what they can do.
const val ITEM_BRIE = "Aged Brie"
const val ITEM_CONJ = "Conjured Mana Cake"
const val ITEM_ELIX = "Elixir of the Mongoose"
const val ITEM_PASS = "Backstage passes to a TAFKAL80ETC concert"
const val ITEM_SULF = "Sulfuras, Hand of Ragnaros"
const val ITEM_VEST = "+5 Dexterity Vest"
enum class TYPE {
AGELES, CHEESE, CONJUR, PASSES, NORMAL
}
enum class PRODUCT(val prodName: String, val type: TYPE) {
PROD_BRIE("Aged Brie", TYPE.CHEESE),
PROD_CONJ("Conjured Mana Cake", TYPE.CONJUR),
PROD_ELIX("Elixir of the Mongoose", TYPE.NORMAL),
PROD_PASS("Backstage passes to a TAFKAL80ETC concert", TYPE.PASSES),
PROD_SULF("Sulfuras, Hand of Ragnaros", TYPE.AGELES),
PROD_VEST("+5 Dexterity Vest", TYPE.NORMAL),
}
I’m just feeling my way here, but this enum has just defined a half-dozen light-weight objects, and I think we can use them to look up the type. Here’s type()
as recast:
private fun Item.type(): TYPE {
val exact = PRODUCT.values().filter { it.prodName == name}
if (exact.isNotEmpty()) return exact[0].type
return TYPE.ERROR
}
This passes all the tests, and if I change “Mana Cake” to “Mamba Cake”, I get errors in my tests including the golden master. What I do not get, however, is an indication in the output line for the Mamba Cake that anything has gone wrong.
I think we’d need to extend Item to do that. I take a chance and do this:
open class Item(var name: String, var sellIn: Int, var quality: Int) {
override fun toString(): String {
return this.name + ", " + this.sellIn + ", " + this.quality + this.status()
}
}
And this:
fun Item.type(): TYPE {
val exact = PRODUCT.values().filter { it.prodName == name}
if (exact.isNotEmpty()) return exact[0].type
return TYPE.ERROR
}
Then if I change to Mamba Cake in my control table … the golden master test says:
-------- day 0 --------
name, sellIn, quality
+5 Dexterity Vest, 10, 20
Aged Brie, 2, 0
Elixir of the Mongoose, 5, 7
Sulfuras, Hand of Ragnaros, 0, 80
Sulfuras, Hand of Ragnaros, -1, 80
Backstage passes to a TAFKAL80ETC concert, 15, 20
Backstage passes to a TAFKAL80ETC concert, 10, 49
Backstage passes to a TAFKAL80ETC concert, 5, 49
Conjured Mana Cake, 3, 6 ERROR TYPE NOT DEFINED
Nearly good. Too much upper case. Revise slightly:
fun Item.status(): String {
return when (type()) {
TYPE.ERROR ->
" ERROR: undefined Product TYPE treated as NORMAL."
else -> ""
}
}
We are green. Commit: All items must be specifically defined in the PRODUCT enumeration.
I’d like to have a direct test for that feature.
@Test
fun `unknown product gets error status`() {
checkUpdate(
"no such product",
10,
30,
29,
" ERROR: undefined Product TYPE treated as NORMAL."
)
}
And I extended the checkUpdate
to check the status, defaulted to “”:
fun checkUpdate(prod: String, sellInStart: Int, qualityStart: Int, qualityEnd: Int, status:String="") {
val item = Item(prod, sellInStart, qualityStart)
val gr = GildedRose(arrayOf(item))
if (true)
gr.updateItems()
else
gr.oldUpdateItem(item)
assertEquals(status, item.status())
assertEquals(sellInStart-1, item.sellIn, "sellIn")
assertEquals(qualityEnd, item.quality, "quality")
}
I’m past my own sell-by date here, right at the two hour mark, and my quality is starting to decrease: I used up my brain on an IDEA problem relating to my setting up the new enum. And we’re at a super stopping point anyway.
Summary
We now have a “table”, actually an enum
, that must include all the names and types of all the items we sell. This is a direct analogy to a database table with similar properties. Our type lookup, for now, expects to find the exact product in the table. If it does not find it, the item is given the TYPE.ERROR, which prices it according to the NORMAL rules, and adds an error message to the text output of the program.
If we should decide to add pattern recognition to product type, such as “anything named with “conjured” in its name is treated as CONJUR, well, we have a place to stand to do that.
As things stand, I don’t see that the fact that the thing is an enum is helping us much, if at all. It could probably be a standard table (wrapped in a class, of course). I’ll think about that: I had thought that the enum would give me more checking than we got. All that’s for another day.
A good morning’s work. See you next time.