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)