Start learning free

LESSON 409 OF 414 · OBJECT-ORIENTED PROGRAMMING

OOP Capstone in Scala: An Animal Hierarchy

Lesson Read the full explanation

TL ; DR:

Put it all together: an abstract parent class, three concrete subclasses, and a polymorphic loop that dispatches by runtime type. This is the shape of nearly every real OOP design.


abstract class Animal(val name: String) {
    def sound(): String            // each subclass picks
    def speak(): Unit = {          // shared by all
        println(s"$name: ${sound()}")
    }
}
// Dog, Cat, Bird each override sound()
// val zoo: Array[Animal] = Array(Dog("Rex"), ...)
// for (a <- zoo) a.speak()


Full lesson:

This problem stitches together everything you've learned in this chapter:

  1. Inheritance — three concrete classes share a common parent.
  2. Constructors — each subclass forwards a name to the parent.
  3. Abstract methods — the parent declares sound() with no body; each child must provide one.
  4. Shared behavior — the parent has a concrete speak() that calls sound(); every subclass inherits it for free.
  5. Polymorphism — a collection typed as Animal holds a Dog, a Cat, and a Bird; speak() dispatches to the right sound() at runtime.

The shape of this code is the shape of nearly every real OOP design you'll see — a small contract at the top (the abstract parent), concrete variants below, and the rest of the program written against the abstract type. New variants get added without touching the consumer code.


Your task

The abstract Animal class is already defined, with a concrete speak() that calls the abstract sound(). Add three subclasses — Dog (returns "Woof"), Cat (returns "Meow"), and Bird (returns "Tweet"). Then in mainFunction, build a polymorphic collection holding one of each (Rex, Whiskers, Tweety in that order) and loop through it calling speak().

Expected output:

Rex: Woof
Whiskers: Meow
Tweety: Tweet
STEP 1 OF 5
    Ctrl + Enter to check

    SCALA · OBJECT-ORIENTED PROGRAMMING

    Oop Capstone in Scala, explained

    What the capstone brings together

    This exercise uses everything from the chapter in one small design:

    1. Inheritance. Dog, Cat and Bird share the parent Animal.
    2. Constructors. Each subclass receives a name and hands it up to the parent.
    3. An abstract method. Animal declares sound() with no body, and each subclass supplies one.
    4. Shared behaviour. Animal has a concrete speak() that calls sound(), so subclasses inherit it unchanged.
    5. Polymorphism. One Array[Animal] holds all three animals, and speak() reaches the right sound() 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 the val also turns it into a public, read-only field. That is why speak can use name.
    • 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 s substitutes $name directly and ${sound()} for any longer expression. With name equal to "Rex" and sound() returning "Woof", the result is Rex: 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:

    1. A constructor parameter name: String in the class header.
    2. 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.
    3. override def sound(): String, so the abstract method is implemented and checked against the parent.
    4. 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. In Dog(name: String), the parameter has no val, so it is only a constructor argument, used here to call Animal(name). The field that speak reads is the parent's val name. Writing val again in the subclass would clash with the parent's field and need override val.
    • return is optional. The last expression of a method is its result, so override def sound(): String = "Woof" means the same. The lesson's form with return and braces is also correct.
    • Mixed array. The annotation Array[Animal] is what allows three different classes in one array.
    • No new needed. Scala 3 lets you write Dog("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 Animal without the argument fails with missing argument for parameter name of constructor Animal.
    • Overriding speak instead of sound. 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 a Cat, and an Array[Dog] can't be used where an Array[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.
    Keep going with ScalaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free