Abstract classes provide developers a place to provide an abstraction point while still being able to include common functionality. In a manner of speaking, an abstract class can be thought of as an unfinished class. It depends on subclasses to finish the class by defining the final behavior of the class. Since abstract classes serve as base classes for concrete classes, we can use them for polymorphism purposes.
Let’s consider a problem that uses abstract classes. We have two cooks. One cook is Bob and he makes burgers. The other cook is Jimmy and he makes pizza. Both cooks have names and own their own restaurants. They also cook food. So in this sense, they both have a lot in common. Although both cooks have the same behavior, cook(), they both cook different things. Bob cooks burgers while Jimmy cooks pizza.
We don’t want to define two classes that are almost completely the same. That would be a really bad practice because then a change in both classes would need to get updated in both places. We also would not be able to define methods that could use both classes without function overloading. In other words, our code would become highly coupled to the implementation of each cook instead of being able to use both cooks generally.
Abstract Class
Here is our Cook class that defines properties and behaviors for both Bob and Jimmy.
abstract class Cook(val name : String, val resturant : String){ /** * This method doesn't have a body. * Child classes must define the cook() method * or they too must be abstract */ abstract fun cook() : String override fun toString(): String { return this.name } }
Classes are made abstract in Kotlin by adding the abstract keyword in front of the class keyword. We will never be able to create an instance of the Cook class due to it being marked abstract. Since our cooks cook different meals, we leave the cook method undefined by marking it as abstract as well. Abstract methods are open by default in Kotlin since they have to be overridden by definition. Since we have an abstract cook() method in Cook, we are forcing all subclasses to define this behavior.
Bob
Bob is a Cook so it makes sense that he subclasses Cook. By extending Cook, we are saying that Bob cooks. However, the Cook class doesn’t say what he cooks or how he cooks. We need Bob to extend Cook and define the cook() method.
class Bob : Cook("Bob Belcher", "Bob's Burgers") { override fun cook(): String { return "Chorizo Your Own Adventure Burger" } }
Since Bob is a cook, he has a name and a restaurant. He not only cooks, but we now know how Bob cooks. Since Bob is a child class of Cook, he can be assigned to any Cook variable.
Jimmy
Jimmy is similar to Bob but he cooks pizzas.
class Jimmy : Cook("Jimmy Pesto", "Petso's Pizzeria"){ override fun cook(): String { return "Boring Pizza" } }
Using Abstract Classes
Now that we have Cook, Bob, and Jimmy defined, we are free to use them in our program. Let’s write a function that takes a Cook, says hello, and tells us what they cook.
fun sayHello(c : Cook){ println("Hello! My name is ${c.name} and I cook ${c.cook()}") }
Since Cook has a name property and a cook() method, the compiler knows that its safe to call cook() on any cook. Which version of the cook() method gets called depends on the runtime type of Cook. Regardless of who is the cook, the program will still work properly.
The sayHello function is said to be loosely coupled to the Cook class. Although we can only actually make Bob or Jimmy objects, the Cook class provides a degree of indirection for developer purposes. If we add other cooks, later on, the sayHello function can utilize those objects as well because they are all members of a common supertype.
Putting it Together
Use abstract classes when
- You need an abstraction point
- You have one or more child classes that have common behavior and properties
- You need to declare behavior but need to define it in child classes
Here is an example program that ties everything together
package ch4.abstractclasses abstract class Cook(val name : String, val resturant : String){ /** * This method doesn't have a body. * Child classes must define the cook() method * or they too must be abstract */ abstract fun cook() : String override fun toString(): String { return this.name } } class Bob : Cook("Bob Belcher", "Bob's Burgers") { override fun cook(): String { return "Chorizo Your Own Adventure Burger" } } class Jimmy : Cook("Jimmy Pesto", "Petso's Pizzeria"){ override fun cook(): String { return "Boring Pizza" } } fun sayHello(c : Cook){ println("Hello! My name is ${c.name} and I cook ${c.cook()}") } fun main(args : Array<String>){ val bob = Bob() val jimmy = Jimmy() sayHello(bob) sayHello(jimmy) }
We get this output when run.
Hello! My name is Bob Belcher and I cook Chorizo Your Own Adventure Burger Hello! My name is Jimmy Pesto and I cook Boring Pizza