Overview:

During the previous tutorial Learn: Kotlin Classes, we covered the basics of a regular Class – functions, properties, constructors. If you are not already familiar with the concept and creation of a Class, start with the previous tutorial as this one builds from it.

In this tutorial, we will be covering some of the more complicated types of classes in Kotlin: Data Classes, Abstract Classes, and Objects.

We will be continuing/refactoring from the previous tutorial project; MoonBucks Cafe. Mostly, we will be refactoring the existing classes as well as adding a Menu for the Cafe, which contains some MenuItems. At the end of this tutorial, you will be able to order from the MoonBucks cafe!

Requirements:

Source Code:
https://github.com/codetober/kotlin-classes-advanced

Abstract Classes

An abstract class is syntactically similar to the definition of a regular class, but, there are some major functional differences. Abstract classes are incredibly handy for enforcing some class structure in your project. Its main purpose is to provide functions and properties to child classes that inherit from the abstract class.

Child classes are required, in order to use the functionality of the abstract class because an abstract class cannot be instantiated! You cannot simply create a new instance of it, you much create a child class that inherits all of its functionality.

In the next example, let’s create an abstract class that will be the parent for all of the items on the MoonBucks menu.

abstract class MenuItem(name: String, price: Double = 0.99, description: String){
    val name: String = name
    val description: String = description
    val price: Double = price

    init {
        require(name.isNotEmpty()){"Cannot Create MenuItem without a valid name!"}
        require(description.isNotEmpty()){"Cannot Create MenuItem without a valid description!"}
        require(price >= 0.0){"Cannot create MenuItem with a negative price!"}
    }
}

Notice that we’ve added the abstract keyword to the class definition. 

So, what we’re doing by creating this class is saying that we will be creating child classes which will inherit all of the functionality of the abstract class. In this case, we want to create specific drinkable items for the MoonBucks Cafe. When we create the child classes, DarkSideFrap and Zero-Gravity Mocha we will simply create a new basic class and append : MenuItem to the class definition and pass all of the required properties directly into the constructor.

class ZeroGravityMocha(
    name: String = "Zero-Gravity Mocha",
    price: Double = 4.99,
    description: String = "It makes you jump higher while on the Moon!") : MenuItem(name, price, description) {
}

Now it’s your turn. Try creating the DarkSideFrap class as a child of MenuItem. In the next part this tutorial we will be creating the Menuand adding the new implementations of our MenuItem abstract class to the menu. 

Data Classes

Similar to the Abstract Class, the Data Class looks very similar to the standard Class definition. Visually, there are only two main differences between a data class and standard class:

  • data keyword at the beginning of the definition
  • data classes require at least one parameter in the primary constructor

However, functionally, there are quite a few differences as data classes:

  • cannot be “inner” or “sealed”
  • may only implement interfaces, cannot be the child of an abstract class
  • has overridable standard functions, like toString(), equals(), toHashCode(), copy()

In this tutorial, we are using the data class to create our Menu. One of the primary reasons for this is so that we can implement our own toString() method to print the Menu Title and all of the items on the Menu.

data class Menu(val name: String){
    private val items: List<MenuItem> = listOf(ZeroGravityMocha(), DarkSideFrap())
    
    fun getItemByNumber(itemNumber: String): MenuItem {
        var item: MenuItem
        try {
            item = items[Integer.parseInt(itemNumber)-1]
        } catch (ex: Exception){
            println("Erm. You ordered the DarkSide, Right? Eh-He..")
            item = DarkSideFrap()
        }
        return item
    }

    override fun toString(): String {
        var menuString = "$name Menu\n-------------------\n"
        items.forEachIndexed { index, menuItem ->
            menuString = menuString.plus("${index+1}.) $${menuItem.price}... ${menuItem.name}... ${menuItem.description} \n")
        }
        menuString = menuString.plus("\nJust tell me the item number you'd like to order! Or say 'exit' to stop ordering.")
        return menuString
    }
}

On line #2 of this code sample, we create a List of MenuItems and populate it with the different beverages created earlier. We’ve also added a function to accept a String like “1, 2, 3, 4, etc…” and return the corresponding the item from our menu. In the case that number does not match an item or causes any other error, we serve the customer a DarkSideFrap.

Lastly, noticed the keyword override. This allowed us to write our own logic for the toString() function. Basically, we print the title of the menu and then iterate through the items and print the relevant information for each.

In the final part of this tutorial we are going to cover the Object, which will represent our Store class from previous tutorials. Refactor the old code, or simply clone the latest from Github and follow along.

Object Declarations

The Object declaration does not include the class keyword. In fact, it only requires you to enter object Store {}. That’s it! Object-ive complete!

Kotlin treats the object as a Singleton. This means that once the object gets created, only one instance will ever exist; the existing instance will be accessed whenever the application needs to use its functions or variables. This makes objects a poor choice for our Menu and MenuItem, but, an excellent choice for our Store; as we have only one Store in the application.

An object is very useful for instantiating some class that is heavy or requires a large amount of computational load to become initialized. An example could be reading from a file or fetching data from a web service.

Our Store is a simple one. The only requirements of the Store object are that it can: print a welcome/exit message, print the menu, and accept a transaction from a customer.

Notice: In case we were loading our menu from a file, the by lazy syntax has been added to the menu property. This will delay the creation of the Menu class until it is referenced; reducing the overhead cost of creating our Store.

object Store {
    val name: String = "MoonBucks Cafe"

    // Properties
    private val menu by lazy { Menu(name) }

    // Functions
    fun printWelcomeMessage() = println("Welcome to $name!\n")
    fun printExitMessage() = println("\nThanks for shopping at $name")
    fun placeOrder(customer: Customer, itemNumber: String?) {
        if(itemNumber != null) {
            println("You ordered: ${menu.getItemByNumber(itemNumber).name} (x1)")
            println("Thanks ${customer.name}! Can I get you anything else?")
        }
    }
    fun printMenu() = println(this.menu.toString())
}

Wrapping it up

To see the fruit of your labor, create a main.kt file (or copy the example below).

Create a new Customer, welcome them to the Cafe, allow them to order from the Menu, and then give them a farewell message. Below is a quick example.

fun main(args: Array<String>) {

    val carat = Customer(name = "Carat")

    Store.printWelcomeMessage()
    Store.printMenu()

    // Loop for user input
    while(true) {
        var input = readLine()
        if(input.equals("exit")){
            break
        } else {
            Store.placeOrder(carat, input ?: "1")
        }
    }

    Store.printExitMessage()
}

For the full code in this tutorial, check it out or clone it from Github.

That’s it! Congratulations on making it all the way through this tutorial and continuing your quest to craft amazing code! If you liked this tutorial, please share it 🙂 Comment below with any questions or issues you faced.