8 minutes read

Sometimes it's not enough to create classes — we also need to build connections between them.

Let's imagine that we have a pet store and we need to write a class that calculates the total cost of the purchase. Now the store only has Cat and Dog, and we can write several functions:

class PriceService:
  def buyCats(quantity: Int): Price = ???
  def buyDogs(quantity: Int): Price = ???

However, we will face problems when we need to sell new kinds of animals, or when a customer wants to buy both a dog and a cat at once. This code won't work in all cases and won't contain the same kind of methods, which will lead to confusion. So how can we generalize the work of a class for different animals? Let's see how traits and abstract classes in Scala can help us with that!

Abstract classes

In Scala, an abstract class serves as a supertype that contains information about subtype classes. They are suitable for specifying what a class is, and we partially fix a behavioral model with this structure.

Just like when we create specific classes, we can pass parameters when creating abstract classes.

For example, we may have an abstract class Pet, and say that Cat is a child class of Pet. Cat has all methods that are described in Pet and can override them:

abstract class Pet(val name: String):
  def sleep: String = "zzzz"
  def speak: String // <- this method has no body

class Cat(name: String) extends Pet(name):
  override def speak: String = "meow"

class Dog(name: String) extends Pet(name):
  override def speak: String = "woof"

As you can see, both Cat and Dog, like all Pet's child classes, have a sleep method whose implementation is defined in an abstract class, but the speak method had to be implemented.

Scala doesn't allow inheriting from multiple abstract classes and the abstract class must be at the begining of the inheritance list. So, adding a price to Pet may not always be convenient: we wouldn't like to get inside the Pet class and attach a specific and non-standard parameter to it — it's better to do it in a more flexible way by adding "behaviors". This is where traits help us.

Traits

Like abstract classes, traits allow using inheritance to reuse behavior in different classes. They are used to share interfaces and fields between classes and to group methods for a given behavior. Traits may also have methods that are fully defined but not implemented.

One of the most essential features of traits is that a class may inherit from multiple traits. Note that in Scala 2 a trait, unlike a class, can't take parameters, but this feature is available in Scala 3 so abstract classes are supported historically and it is better to use traits.

type Price = Int

trait WithPrice:
  def price: Price

class Cat(name: String) extends Pet(name), WithPrice: // or `extends Pet(name) with WithPrice` in Scala 2
  override def speak: String = "meow"
  override def price: Price = 100

class Dog(name: String) extends Pet(name), WithPrice:
  // We can skip the "override" keyword if the trait method has no body
  def speak: String = "woof"
  def price: Price = 200

class FullPrice(val amount: Price = 0):
  def add(item: WithPrice): FullPrice = FullPrice(amount + item.price)

println {
  FullPrice()
    .add(Cat("Bubble"))
    // we can add not only a cat, and not even just a pet
    .add(Dog("Dummy"))
    .amount
} // 300

We slipped in a new additional behavior in the Cat and Dog classes without changing their parent class Pet, which allowed us to treat specific classes like some entities that have a price. This made our code more readable and easy to use.

Default implementations

Abstract classes and traits require us to fully implement their functionality, that is, write an implementation of declared methods, which makes it more convenient to work with entities at the interface level. But we can also define a basic implementation so that we don't have to override it every time it's the same for several child classes — both traits and abstract classes give us this possibility.

trait WithPrice:
  private val shopDefaultPrice = 100
  def price: Price = shopDefaultPrice

trait FreePrice extends WithPrice:
  override val price: Price = 0

Conclusion

In this topic, we considered two approaches to reusing behavior with inheritance in Scala: abstract classes and traits. You saw the usages of each of them and learned about their main differences so you can understand which to choose for specific tasks.

4 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo