What the capstone brings together
This exercise uses everything from the chapter in one small design:
- Inheritance.
Dog,CatandBirdshare the parentAnimal. - Constructors. Each subclass receives a name and hands it up to the parent.
- An abstract method.
Animaldeclaressound()with no body, and each subclass supplies one. - Shared behaviour.
Animalhas a concretespeak()that callssound(), so subclasses inherit it unchanged. - Polymorphism. One
Array[Animal]holds all three animals, andspeak()reaches the rightsound()at run time.
The expected output, with the animals in order, is:
Rex: Woof
Whiskers: Meow
Tweety: Tweet
The parent class
The starter code already defines Animal:
abstract class Animal(val name: String) {
def sound(): String
def speak(): Unit = {
println(s"$name: ${sound()}")
}
}
Three Scala details sit in these few lines:
- The constructor is part of the class header.
(val name: String)declares a constructor parameter, and thevalalso turns it into a public, read-only field. That is whyspeakcan usename. sound()has no body, which makes it abstract and the class abstract.speak()has a body and is shared.- String interpolation. A string that starts with
ssubstitutes$namedirectly and${sound()}for any longer expression. Withnameequal to"Rex"andsound()returning"Woof", the result isRex: Woof.
This is the template-method idea: speak fixes the output format once, and the subclasses only fill in the part that varies.
Step by step
Step 1: three subclasses
Each class needs the same four pieces:
- A constructor parameter
name: Stringin the class header. extends Animal(name), which passes the name up to the parent's constructor. A subclass has to call the parent's constructor, and the arguments go right after the parent's name.override def sound(): String, so the abstract method is implemented and checked against the parent.- A body that produces the right text:
"Woof","Meow"or"Tweet".
The subclasses don't override speak. It is inherited, and it calls their sound.
Step 2: the polymorphic loop
In mainFunction, declare val animals: Array[Animal] and fill it with Dog("Rex"), Cat("Whiskers") and Bird("Tweety"), in that order. Then loop with for (a <- animals) and call a.speak(). The loop is written against Animal only. It has no idea which animals are in the array.
The solution in Scala
abstract class Animal(val name: String) {
def sound(): String
def speak(): Unit = {
println(s"$name: ${sound()}")
}
}
class Dog(name: String) extends Animal(name) {
override def sound(): String = { return "Woof" }
}
class Cat(name: String) extends Animal(name) {
override def sound(): String = { return "Meow" }
}
class Bird(name: String) extends Animal(name) {
override def sound(): String = { return "Tweet" }
}
class Solution {
def mainFunction(): Unit = {
val animals: Array[Animal] = Array(Dog("Rex"), Cat("Whiskers"), Bird("Tweety"))
for (a <- animals) {
a.speak()
}
}
}
Notes on the Scala involved:
- Two things called
name. InDog(name: String), the parameter has noval, so it is only a constructor argument, used here to callAnimal(name). The field thatspeakreads is the parent'sval name. Writingvalagain in the subclass would clash with the parent's field and needoverride val. returnis optional. The last expression of a method is its result, sooverride def sound(): String = "Woof"means the same. The lesson's form withreturnand braces is also correct.- Mixed array. The annotation
Array[Animal]is what allows three different classes in one array. - No
newneeded. Scala 3 lets you writeDog("Rex")to create an instance.
Why the design pays off
To add a cow, write class Cow(name: String) extends Animal(name) with its own sound, and add one to the array. The loop is not touched, and neither is Animal. If Cow forgot sound, the compiler would refuse the class, because the abstract method would be unimplemented.
Common mistakes
- Not passing the name up. Writing
extends Animalwithout the argument fails withmissing argument for parameter name of constructor Animal. - Overriding
speakinstead ofsound. That repeats the formatting in every class and defeats the shared method. - Wrong order. The output follows the array: Rex, Whiskers, Tweety.
- Typing the array as a subclass. An
Array[Dog]can't hold aCat, and anArray[Dog]can't be used where anArray[Animal]is required.
Key takeaways
- An abstract parent fixes the contract and the shared behaviour; subclasses fill in the differences.
- A subclass passes constructor arguments to its parent with
extends Parent(args). - Code written against the parent type works for every subclass, so new variants need no changes to it.
