Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this happen a lot when developers move from a language like Python to Scala. They try to build a class hierarchy that reflects the real world, but they hit a wall because they're thinking in terms of classes rather than behaviors. Take a look at this snippet from a game project I reviewed recently.

class Entity(val name: String) {
  def describe: String = s"I am $name"
}

class Combatant {
  def attack(): Unit = println("Attacking!")
}

class Human {
  def speak(): Unit = println("Hello there!")
}

// This line causes a compilation error
class Warrior(name: String) extends Entity(name), Combatant, Human {
  def specialMove(): Unit = println(" Whirlwind Attack!")
}

The Multiple Inheritance Wall

If you try to compile this, the compiler will scream at you. In Scala, a class can only extend one other class. You can't have a Warrior that is simultaneously an Entity, a Combatant, and a Human if all three are defined as classes. The developer here was trying to use inheritance to say "A Warrior is all of these things," but Scala (and the JVM) doesn't allow multiple class inheritance because it creates a nightmare called the "Diamond Problem," where the compiler wouldn't know which parent's method to use if two parents defined the same logic.

Mixing in Behavior with Traits

The fix isn't to change the architecture, but to change the tool. We use Traits. Think of a trait as a "blueprint for behavior." While a class defines what an object is, a trait defines what an object can do.

Here is how we fix that code. We keep Entity as the base class (because every single thing in our game is an entity), but we turn Combatant and Human into traits.

class Entity(val name: String) {
  def describe: String = s"I am $name"
}

trait Combatant {
  def attack(): Unit = println("Attacking!")
}

trait Human {
  def speak(): Unit = println("Hello there!")
}

// Now this works perfectly
class Warrior(name: String) extends Entity(name) with Combatant with Human {
  def specialMove(): Unit = println("Whirlwind Attack!")
}

Notice the syntax change: we use extends for the first (and only) class, and then we use with for every trait we want to "mix in." I personally love this pattern because it allows you to compose your objects like Lego bricks. You can add Flight, Magic, or Swimming traits to any class without breaking your inheritance tree.

Abstract vs. Concrete Trait Methods

In the example above, the traits had concrete methods (they had a body with println). But traits get really powerful when you define abstract methods—methods that have no implementation and must be defined by the class that uses the trait.

Let's say we want all Combatants to have a powerLevel, but that level is different for a Warrior than it is for a Mage. We can leave it abstract in the trait:

trait Combatant {
  def powerLevel: Int // Abstract method: no body here!
  
  def attack(): Unit = {
    println(s"Attacking with a power of $powerLevel!")
  }
}

class Warrior(name: String) extends Entity(name) with Combatant {
  override val powerLevel: Int = 100
}

class Mage(name: String) extends Entity(name) with Combatant {
  override val powerLevel: Int = 150
}

This is a subtle but important distinction. The attack method is shared logic (concrete), but it relies on powerLevel, which is specific to the implementation (abstract). This allows you to define a template for behavior while letting the final class fill in the blanks.




📋 Practical Task

Building a Smart Home Device System

You are designing a system for smart home gadgets. Some devices can be controlled via Wi-Fi, and some can be powered on and off. Not every device has both capabilities (e.g., a smart bulb is both, but a simple battery-powered sensor might only be "Powerable").

Your task:

  • Create a base class Device that takes a name: String and has a method status() that returns "Device [name] is active".
  • Create a trait Connectable with:
    • An abstract method ipAddress: String.
    • A concrete method connect() that prints "Connecting to [ipAddress]...".
  • Create a trait Powerable with a concrete method togglePower() that prints "Power toggled".
  • Implement a class SmartBulb that extends Device and mixes in both Connectable and Powerable. Ensure you provide the ipAddress implementation.
  • Implement a class BatterySensor that extends Device and only mixes in Powerable.

Test your code by instantiating both a SmartBulb and a BatterySensor and calling their respective methods.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.