1. Overview

In this tutorial, we’re going to cover companion objects in Scala, what they are, and how can be used to implement the factory pattern and extractors.

2. Class and Object

Let’s assume we’re working with a Task class:

class Task(val description: String) {
  private var _status: String = "pending"

  def status() : String = _status
}

Task is a basic Scala class, if we want to create an object of Task, we do so by calling its primary constructor:

val task = new Task("do something")
assert(task.description == "do something")

Now, let’s add another constructor to the class that accepts status as a parameter:

class Task(val description: String) {
  private var _status: String = "pending"

  def this(description: String, status: String) = {
    this(description)
    this._status = status
  }

  def status():String = _status
}

This allows us to create Task objects with a given status:

val task = new Task("do something", "started")
assert(task.status == "started")

We can add more auxiliary constructors but there are a couple of issues:

  • Using the new keyword every time we create an object, although it’s a minor detail, it’d nice to avoid it
  • All auxiliary constructors must call an existing constructor as the first expression resulting in constructor telescoping. Wouldn’t it be nice to add factory methods to create a fluent API to create objects easily? Factory methods offer many advantages over constructors

We could overcome the above drawbacks if we used static factory methods.

But Scala doesn’t provide a static modifier. In Scala, every value is an object, and every operation a method. Having statics breaks that rule. Instead, Scala has singleton objects.

Let’s see how we can use singleton objects to address the previous drawbacks.

3. Companion Objects

When a singleton object shares the same name with a class, it’s called that class’s companion object. Both class and companion object must be defined in the same source file.

Let’s define a companion object for the Task class we created earlier:

class Task(val description: String) {
  private var _status: String = "pending"
  def status():String = _status
}

object Task {
  def apply(description: String): Task = new Task(description)
}

We added a singleton object with the same name. Task singleton object has an apply method. Recall that an apply method is a special method in Scala. It’s an injector that can be invoked without the method name.

When we add an apply method to a companion object it allows us to create objects without using the new keyword:

val task = Task("do something")
assert(task.description == "do something")

This is similar to calling the apply method. When an argument list is put after an object, Scala looks for an apply method that matches the argument list.

Let’s verify that:

val task = Task.apply("do something")
assert(task.description == "do something")

We can overload the apply method to create different factory methods with different parameters.

Let’s add another apply method that takes a status parameter:

class Task(val description: String) {
  private var _status: String = "pending"

  def status():String = _status
}

object Task {
  def apply(description: String): Task = new Task(description)

  def apply(description: String, status: String): Task = {
    val task = new Task(description)
    task._status = status
    task
  }
}

We overloaded apply method that in addition to description takes the status parameter as well:

val task = Task("do something", "started")
assert(task.status == "started")

Notice that in the above example companion object was able to access the _status field which is marked as private. This is one major advantage of companion objects, a class and its companion object can access each other’s members, even private.

Companion objects can implement more sophisticated apply methods to create objects covering a whole class hierarchy. A parent class object can choose to create a particular subtype that is more suitable for a scenario. A factory can hide all this logic to give a uniform interface to the user.

This is a significant advantage over auxiliary constructors which only return an object of the same type.

4. Extractors

Similar to the apply method, we can add an unapply methods to companion objects that act as extractor which lets us de-construct an object instance.

Let’s add an extractor that returns the description and status of a Task:

object Task {
  def unapply(task: Task): Tuple2[String, String] = (task.description, task.status())
}

val task = Task("do someting")
val (description, status) = Task.unapply(task)
assert(description == "do something")
assert(status == "pending")

We can see unapply lets us extract description and status out of a Task object. The unapply method can return anything and enable powerful pattern-matching expressions.

5. Conclusion

In this article, we learned about companion objects and how they can be used to create factory builder methods.

We also looked at how companion object can access private members of the class and vice versa. We saw how to implement unapply methods to enable extractors.

As always, all code examples can be found over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments