7 Families of Concepts: Inheritance with traits
As you build more complex software, you’ll start to notice patterns. Imagine you’re creating a system to manage company files. You might start by building a WordDocument
class, a Spreadsheet
class, and a PdfDocument
class. Soon, you’ll realize they all need a print()
method, they all have a fileSize
, and they all need to be opened()
.
Are you going to copy and paste the fileSize
logic and the print()
method into all three classes? The moment you do, you create a maintenance nightmare. If you need to update the printing logic, you’ll have to find and change it in three different places. This violates a core principle we’ve learned: Don’t Repeat Yourself (DRY).
There has to be a better way—and there is. It’s called inheritance.
Inheritance is the mechanism for creating “IS-A” relationships between classes. A PdfDocument
IS A type of Document
. A Car
IS A type of Vehicle
. This allows us to define common behaviors and attributes in one central place, promoting code reuse and creating logical, understandable families of classes.
In modern Scala, the primary tool for this is the trait
.
7.1 What is a trait
? More Than Just a Contract
We’ve briefly described a trait
as a contract. Let’s create a richer analogy.
- Analogy: Roles and Abilities Think of a person. A person can have multiple roles or abilities. Someone can be an
Employee
, aParent
, and aMusician
.- The
Employee
role comes with abilities likeattendMeeting()
andsubmitReport()
. - The
Parent
role comes with abilities likeprepareLunch()
. - The
Musician
role comes with abilities likeplayInstrument()
.
trait
in Scala is like one of these roles. It’s a bundle of behaviors and characteristics that a class can “mix in” to gain new abilities. Crucially, a class can mix in multiple traits, just as a person can have many roles. - The
A trait
can provide two kinds of members to a class:
- Abstract Members (The “What”): These are contractual obligations. The
trait
defines what must be done, but not how. It’s a method or value that the class must implement itself. This is the contract part of the analogy. - Concrete Members (The “How”): These are default, implemented methods and values. The
trait
provides common, shared behavior that a class gets for free, just by extending the trait. This is the code reuse part.
7.2 Hands-On: Modeling a Family of Documents
Let’s build a rich example that shows both of these powers in action. We will model a file system.
7.2.1 Step 1: The Core Blueprint — The Document
trait
First, we’ll define the essential characteristics that all documents in our system must have.
// This trait defines the core concept of a "Document".
trait Document {
// 1. An ABSTRACT value. Any class extending Document MUST define this.
val filename: String
// 2. An ABSTRACT method. The class must provide its own way to open.
def open(): Unit
// 3. A CONCRETE method. Any class extending Document gets this for FREE.
// Notice how it can use the abstract 'filename' value, even though it's
// not defined here. It trusts that the implementing class will provide it.
def save(): Unit = {
println(s"Saving document: $filename...")
// In a real system, file-saving logic would go here.
}
}
This trait
is a beautiful mix. It forces implementing classes to define their own filename
and open
behavior, while giving them a shared, pre-built save
method.
7.2.2 Step 2: Implementing the Concrete Classes
Now, let’s create two different types of documents that fulfill the Document
contract.
// A PdfDocument IS A Document.
class PdfDocument(val filename: String) extends Document {
// We MUST implement the abstract 'open' method.
override def open(): Unit = {
println(s"Opening PDF file '$filename' in a PDF reader.")
}
}
// A WordDocument IS A Document.
class WordDocument(val filename: String) extends Document {
// It has its OWN, different implementation of 'open'.
override def open(): Unit = {
println(s"Opening Word file '$filename' in Microsoft Word.")
}
}
Let’s test them out:
val myResume = new PdfDocument("resume_final.pdf")
val myReport = new WordDocument("q3_sales_report.docx")
.open() // Calls the PDF-specific open method
myResume.open() // Calls the Word-specific open method
myReport
.save() // Calls the SHARED save method from the Document trait
myResume.save() // Also calls the SHARED save method myReport
7.2.3 Step 3: Adding More Abilities with Mixins
What if only some documents are printable? We can define that ability in its own separate trait
.
// This trait defines the ROLE or ABILITY of being printable.
trait Printable {
// This is a concrete method. Any class that is Printable gets it.
def print(): Unit = {
println("Sending to the default printer...")
}
}
Now, we can “mix in” this Printable
ability to the classes that need it using the with
keyword.
// A PDF is a Document AND it is Printable.
class PdfDocument(val filename: String) extends Document with Printable {
override def open(): Unit = {
println(s"Opening PDF file '$filename' in a PDF reader.")
}
}
// A Word Document is also a Document AND Printable.
class WordDocument(val filename: String) extends Document with Printable {
override def open(): Unit = {
println(s"Opening Word file '$filename' in Microsoft Word.")
}
}
// A Video File IS A Document, but it is NOT Printable.
class VideoFile(val filename: String) extends Document {
override def open(): Unit = {
println(s"Playing video file '$filename' in a media player.")
}
}
Now, our PdfDocument
and WordDocument
have gained a new print()
method, while VideoFile
has not. This is the power of mixing in roles.
val finalContract = new PdfDocument("signed_contract.pdf")
val movieClip = new VideoFile("launch_video.mp4")
.print() // This works!
finalContract// movieClip.print() // ERROR! This line would not compile. A VideoFile is not Printable.
7.3 Context and Best Practices
Tip 1: Use
trait
s for Behaviors and Roles Think of traits as adjectives or abilities. Good names for traits are often nouns ending in “-able” or “-er” (e.g.,Printable
,Clickable
,Serializable
,Logger
). You use them to add capabilities to a class. Classes, in contrast, represent concrete things or nouns (Customer
,Invoice
).Tip 2: Why
trait
s are often better thanabstract class
You might see another keyword in older Scala code or other languages:abstract class
. It’s similar to atrait
, but with one major limitation: a class can onlyextend
one other class. However, a class canextend
manytrait
s (e.g.,... extends Document with Printable with Serializable
). This makestrait
s far more flexible for composing behaviors, like snapping together building blocks of functionality.Tip 3: Designing to an Interface When you use a
trait
, you are practicing a powerful design principle called “coding to an interface.” You are defining a general contract (Document
) that other parts of your system can rely on, without needing to know the specific details of the concrete classes (PdfDocument
,WordDocument
). This “decoupling” makes your code much easier to change and extend later on.
We have now successfully created a “family” of related but distinct classes. They are all Document
s and share a common contract, but they can also have their own unique abilities. This sets the stage perfectly for the next chapter, where we will learn how to leverage this family structure to write incredibly flexible and powerful code.