Architecture Learnings #6, Design Patterns — Builder vs Decorator and the Pizza Problem

Image by Michal Jarmoluk from Pixabay

One of the common design questions in Software Engineering interviews is the Pizza problem. i.e Building the software for a pizza shop who has a wide variety of pizza and wants to prepare different types, e.g Veg Pizza, Non-veg Pizza, Flat Bread, Pepperoni Pizza with Extra Cheese 😋, put add on toppings on it.

Hungry??? So am I… please wait for some more time.

Let’s now see the different approaches to solve this design problem.

Traditionally, for pizza problem, Builder Pattern is most commonly used. However, there are some examples using Decorator Pattern as well. Both the approaches are correct but there is difference in use case which we will discuss in detail in this article.

So what is Builder Pattern?

Builder is an object creation pattern. Once created, we ideally do not want to modify the object. It also helps us when more than one object having the same attributes has to be created or incremental object creation with all or some attributes needs to be achieved.

Parts of the Builder pattern

  1. Product: The product needs to be created (or built). The constructor of this class is private in order to only allow creation through the inner builder class.
  2. Builder: The class responsible for setting the attributes of the product(refer #1). At first, the mandatory attributes are passed in the constructor. Then, optional arguments can be set using setters, in any order, with method chaining. Finally, the build() method is invoked to return the Product with the specified attributes.
  3. Client: This class uses the builder to create an object of the product class.

UML Diagram

Image by Camilo Antonio Moreno from Pixabay

Solving the Pizza problem with the Builder Pattern

As shown in the UML diagram, let’s create the possible attributes for building our Pizza.

abstract class PizzaAttribute {
abstract fun getName(): String
abstract fun getCost(): Float
}


sealed class Size: PizzaAttribute() {
object Small : Size() {
override fun getName() = "Small Size Pizza"
override fun getCost() = 100.00F
}

object Medium : Size() {
override fun getName() = "Medium Size Pizza"
override fun getCost() = 200.00F
}

object Large : Size() {
override fun getName() = "Large Size Pizza"
override fun getCost() = 300.00F
}
}

sealed class Topping: PizzaAttribute() {
object Pepperoni : Topping() {
override fun getName() = "Pepperoni Topping"
override fun getCost() = 10.00F
}

object Mushroom : Topping() {
override fun getName() = "Mushroom Topping"
override fun getCost() = 30.00F
}

object Chicken : Topping() {
override fun getName() = "Chicken Topping"
override fun getCost() = 20.00F
}
}

sealed class Crust: PizzaAttribute() {
object Thin : Crust() {
override fun getName() = "Thin Crust"
override fun getCost() = 30.00F
}

object Stuffed : Crust() {
override fun getName() = "Stuffed Crust"
override fun getCost() = 50.00F
}

object Thick : Crust() {
override fun getName() = "Thick Crust"
override fun getCost() = 40.00F
}
}

sealed class Cheese: PizzaAttribute() {
object American : Cheese() {
override fun getName() = "American Cheese"
override fun getCost() = 35.00F
}

object Italian : Cheese() {
override fun getName() = "Italian Cheese"
override fun getCost() = 55.00F
}
}

Next, we create the Pizza Builder as below

package com.pizza.builder

class PizzaBuilder {
var contents: MutableList<PizzaAttribute> = mutableListOf()

fun withSize(size: Size): PizzaBuilder {
addPizzaAttribute(size)
return this
}

fun withTopping(topping: Topping): PizzaBuilder {
addPizzaAttribute(topping)
return this
}

fun withCrust(crust: Crust): PizzaBuilder {
addPizzaAttribute(crust)
return this
}

fun withCheese(cheese: Cheese): PizzaBuilder {
addPizzaAttribute(cheese)
return this
}

private fun addPizzaAttribute(size: PizzaAttribute) {
contents.add(size)
}

fun build() = PizzaProduct(this)
}

The Pizza Product Class looks something like this.

package com.pizza.builder

class PizzaProduct(private val pizzaBuilder: PizzaBuilder) {
fun getTotalCost() : Double {
return pizzaBuilder.contents.sumByDouble {
it
.getCost().toDouble()
}
}

fun getPizzaInfo() : String {
return pizzaBuilder.contents.joinToString {
it
.getName()
}
}
}

And, finally the Pizza Client

package com.pizza.builder

fun main(args: Array<String>) {
val productOne = PizzaBuilder()
.withSize(Size.Medium)
.withCheese(Cheese.American)
.withCrust(Crust.Stuffed)
.withTopping(Topping.Chicken).build()

val productTwo = PizzaBuilder()
.withSize(Size.Small)
.withCheese(Cheese.American).build()

println(productOne.getPizzaInfo() +" cost : "+productOne.getTotalCost())

println(productTwo.getPizzaInfo() +" cost : "+productTwo.getTotalCost())
}

Voila !!! After executing the above Pizza Client you will be able to see the following as output:

Medium Size Pizza, American Cheese, Stuffed Crust, Chicken Topping cost : 305.0
Small Size Pizza, American Cheese cost : 135.0

Introduction to Decorator Pattern

The decorator pattern is a design pattern that allows behaviour to be added to an individual object, dynamically, without affecting the behaviour of other objects from the same class.

If you consider our ongoing use case, it would be that some base pizza product is first prepared and then different attributes are added to it.

Parts of the Decorator pattern

  1. Component Interface: The interface or abstract class defining the methods that will be implemented.
  2. Concrete Component: The basic implementation of the component interface.
  3. Decorator: The Decorator class implements the component interface and it has a HAS-A relationship with the component interface. The component variable should be accessible to the child decorator classes.
  4. Concrete Decorator: Extending the base decorator functionality and modifying the component behaviour as per need.

UML Diagram for Decorator Pattern

Solving the Pizza problem with the Decorator Pattern

As shown in the UML diagram, let’s decorate the Pizza. Firstly, we will create the component interface or abstract class viz. AbstractPizza

package com.pizza.decorator

interface AbstractPizza {
fun getName(): String
fun getCost(): Double
}

Then, let’s create a concrete BasePizza class as below

package com.pizza.decorator

class BasePizza: AbstractPizza {
override fun getName() = "Base Pizza"
override fun getCost() = 0.00
}

Now, let’s create the base decorator class extending the AbstractPizza class and AbstractPizza as composition.

open class BasePizzaDecorator(private val pizza: AbstractPizza) : AbstractPizza {
override fun getName() = pizza.getName()

override fun getCost() = pizza.getCost()
}

It’s time to create the concrete decorators extending the BasePizzaDecorator class created above.

class SmallSizePizzaDecorator(private val pizza: AbstractPizza): BasePizzaDecorator(pizza) {
override fun getName(): String {
return pizza.getName() + " Small "
}

override fun getCost(): Double {
return pizza.getCost() + 100.00
}
}

class ChickenToppingDecorator(private val pizza: AbstractPizza): BasePizzaDecorator(pizza) {
override fun getName(): String {
return pizza.getName() + " With Chicken Toppings "
}

override fun getCost(): Double {
return pizza.getCost() + 20.00
}
}

class AmericanCheeseDecorator(private val pizza: AbstractPizza): BasePizzaDecorator(pizza) {
override fun getName(): String {
return pizza.getName() + " With American Cheese "
}

override fun getCost(): Double {
return pizza.getCost() + 20.00
}
}

class StuffedCrustDecorator(private val pizza: AbstractPizza): BasePizzaDecorator(pizza) {
override fun getName(): String {
return pizza.getName() + " With Stuffed Crust "
}

override fun getCost(): Double {
return pizza.getCost() + 200.00
}
}

Let’s now decorate our BasePizza by using our client class

package com.pizza.decorator

fun main(args: Array<String>) {
val finalPizza = StuffedCrustDecorator(
ChickenToppingDecorator(
AmericanCheeseDecorator(
SmallSizePizzaDecorator(
BasePizza()
)
)
)
)

println(finalPizza.getName())
println(finalPizza.getCost())
}

On running the above code snippet, you can see the below output:

Base Pizza Small With American Cheese With Chicken Toppings With Stuffed Crust
340.0

Conclusion

So, one should use Builder pattern if he wants to limit the object creation with certain attributes. e.g there are some attributes which are mandatory to be set before the object is created or we want to freeze object creation until certain attributes are not set yet. Once the product is built, it cannot be modified.

Decorator, on the other hand, is used to add additional attributes of an existing object to create a new object as we have seen in the example above. Unlike Builder, there is no restriction of finalising the object until all its attributes are added.

So considering the use case and requirement, you can opt for any one of them.

Thanks for reading this article. Hope, you have learnt something out of it. Please do not forget to hit the clap button as many times as you liked the content.

--

--

--

Mobile Apps Engineering "When we share, we open doors to a new beginning”

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Harry Potter Invisibility Cloak Using Python OpenCV — OindrilaSen

Worst Way to Write Pandas Dataframe to Database

Announcing The Magikcraft Open Source Platform

Strategic domain driven design and enterprise architecture

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store

Hitesh Das

Mobile Apps Engineering "When we share, we open doors to a new beginning”

More from Medium

Linting tools as a method of improving code quality in robust systems

Software Development Models: Comparing Waterfall & Feedback Models

Flaky (as a good pastry) tests.

Why are we still using RDBMs?