Adding pizzas

Now, let's add the ability to add a pizza to our order, as well as configure it:

  1. First, we'll create a Topping class, which will be contained within our Pizza class:
sealed class Topping(name: String): Item(name)
object Pepperoni : Topping("Pepperoni")
object Olive : Topping("Olive")
object Pineapple : Topping("Pineapple")
  1. Now, we'll add a Pizza class:
sealed class Pizza(name: String) : Item(name) {
val toppings: MutableList<Topping> = mutableListOf()
}

This Pizza class extends Item and has a field, MutableList<Topping>, for storing our pizza toppings. 

  1. Next, we're going to create some predefined types of pizza that our users can order:
class BuildYourOwn(init: Pizza.() -> Unit = {}) : 
Pizza("Build Your Own Pizza") {
init {
init.invoke(this)
}
}
class PepperoniPizza(init: Pizza.() -> Unit = {}) :
Pizza("Pepperoni Pizza") {
init {
toppings.add(Pepperoni)
init.invoke(this)
}
}

Both classes take an init argument, which is a function with a Pizza receiver. This function is then called during the class initialization, giving the creators of the class a chance to configure the instance. The PepperoniPizza class also adds a default topping to the pizza.

If we add the unaryPlus operator for the Topping class, we can simplify how we add a topping within the context of configuring our Pizza instance:

operator fun Topping.unaryPlus() = toppings.add(this)
class PepperoniPizza(init: Pizza.() -> Unit = {}) :
Pizza("Pepperoni Pizza") {
init {
+Pepperoni
init.invoke(this)
}
}

Now, we're ready to add a Pizza to our order. To start, we'll create a new method on the Order class:

fun pizza(init: Pizza.() -> Unit) {
val pizza = BuildYourOwn()
pizza.init()
items[pizza] = 1
}

This method takes a function type with the Pizza receiver so that the Pizza can be configured. It also creates a default BuildYourOwn instance and adds the Pizza to the items map. With that in place, we can now add a pizza to our order:

val order = order {
soda(Coke)
+Sprite
Dr_Pepper quantity 2

pizza {
+Pineapple
}
}

What if we want to add a predefined Pizza and then configure it ourselves? To do that, we can add the unaryPlus operator for Pizza within our Order class:

operator fun Pizza.unaryPlus() {
items.put(this, items.getOrDefault(this, 0) + 1)
}

Now, we can use the + operator to add the pizza and then still pass in our configuration block to update the toppings:

val order = order {
soda(Coke)
+Sprite
Dr_Pepper quantity 2

pizza {
+Pineapple
}

+HawaiianPizza {
+Pepperoni
}
}

We now have several ways of adding both Soda and Pizza instances to our order. Now, let's take a look at adding @DslMarker to ensure that our configuration blocks default functionality that is specific to the most local scope.