So far, so good. Our bakery looks good. However, we have a problem with our current model. Let's look at the following code:
fun main(args: Array<String>) {
val anyGood = BakeryGood("Generic flavour")
}
We can instantiate the BakeryGood class directly, which is too generic. To correct this situation, we can mark BakeryGood as abstract:
abstract class BakeryGood(val flavour: String) {
fun eat(): String {
return "nom, nom, nom... delicious $flavour ${name()}"
}
open fun name(): String {
return "bakery good"
}
}
An abstract class is a class designed solely to be extended. An abstract class can't be instantiated, which fixes our problem.
What makes abstract different from open?
Both modifiers let us extend a class, but open lets us instantiate while abstract does not.
Now that we can't instantiate, our name() method in the BakeryGood class isn't that useful anymore, and all our subclasses, except for CinnamonRoll, override it anyway (CinnamonRoll relays on the Roll implementation):
abstract class BakeryGood(val flavour: String) {
fun eat(): String {
return "nom, nom, nom... delicious $flavour ${name()}"
}
abstract fun name(): String
}
A method marked as abstract doesn't have a body, just the signature declaration (a method signature is a way to identify a method). In Kotlin, a signature is composed of the method's name, its number, the type of parameters, and the return type.
Any class that extends BakeryGood directly must override the name() method. The technical term for overriding an abstract method is implement and, from now on, we will use it. So, the Cupcake class implements the name() method (Kotlin doesn't have a keyword for method implementation; both cases, method implementation, and method overriding, use the keyword override).
Let's introduce a new class, Customer; a bakery needs customers anyway:
class Customer(val name: String) {
fun eats(food: BakeryGood) {
println("$name is eating... ${food.eat()}")
}
}
fun main(args: Array<String>) {
val myDonut = Donut("Custard", "Powdered sugar")
val mario = Customer("Mario")
mario.eats(myDonut)
}
The eats(food: BakeryGood) method takes a BakeryGood parameter, so any instance of any class that extends the BakeryGood parameter, it doesn't matter how many hierarchy levels. Just remember that we can instantiate BakeryGood directly.
What happens if we want a simple BakeryGood? For example, testing.
There is an alternative, an anonymous subclass:
fun main(args: Array<String>) {
val mario = Customer("Mario")
mario.eats(object : BakeryGood("TEST_1") {
override fun name(): String {
return "TEST_2"
}
})
}
A new keyword is introduced here, object. Later on, we'll cover object in more detail, but for now, it is enough to know that this is an object expression. An object expression defines an instance of an anonymous class that extends a type.
In our example, the object expression (technically, the anonymous class) must override the name() method and pass a value as the parameter for the BakeryGood constructor, exactly as a standard class would do.
Remember that an object expression is an instance, so it can be used to declare values:
val food: BakeryGood = object : BakeryGood("TEST_1") {
override fun name(): String {
return "TEST_2"
}
}
mario.eats(food)