Scala Foundation Course - Scala Companion Objects


Welcome back. In the earlier video, I talked about Scala Objects and two different type of their usage.

  1. Collection of Static methods
  2. Singleton Object

The third type of common practice is to use Scala objects as a companion object for a class.

What is a companion object?

Well, the idea is simple.
A companion object is nothing but an extension of the singleton object. If we define a Scala class and a Scala object with the same name in the same source file, they become a companion of each other. So, defining a companion object is quite simple. There are just two rules.

  1. Class name and the Object name are same.
  2. The class and the object, both reside in the same source file.

Why do we need companion objects

Before we look at an example, let's try to understand the reason for having a companion object? What purpose do they serve?
There are two main reasons.

  1. Separation of concerns using a Companion
  2. Implementing Factory Method or a Builder pattern

Let's talk about the first point. Java allows you to mix-in static and non-static members in the class definition. But Scala's approach is to separate them into two different constructs. You can define all instance-level fields and methods in a Class. If you have some global static fields and methods, define them in the object. And keep them together into the same file but still separate them into two different constructs. That separation makes your design more understandable. It is like you have a pair of companions. A Class and its companion Object. They go everywhere together. The Object takes care of all the static concerns, and the class takes care of all non-static concerns. Two bodies but a single soul, made for each other. Simple as that.
Let's take an example to understand the implementation.

                                
    class Graph(path:String){        
        println("Load the Graph from file")   
        def numEdges = 506
        def numVertices = 305
        def persist(storageLevel: Int) = println("Returns a new persisted Graph")
    }
                                        
    val g = new Graph("file_location")
    g.persist(Graph.MEMORY_ONLY)                                        
                            

Suppose you are planning to create a class for a graph. Let's define a primary constructor that takes a file location. Now, I need to define the constructor body. The constructor body should open the file and load the graph from the file. For the sake of simplicity, let's just place a print statement. You already know that the class body is also the constructor body and this print statement represents the constructor code. Right?
Let me create a couple of methods. The first method numEdges doesn't take any arguments and returns the number of edges in the graph. I really don't know how to count the number of edges in this graph, so I am simply returning an imaginary number. Similarly, numVertices is counting the vertices and returning the same.
Finally, I have a persist method. This example takes inspiration from Spark libraries, and hence the persist method offers five different options. You can specify those options using the storage level parameter. The parameter is primarily an integer type ranging from 0 to 4. However, I wanted to give them a meaningful name. The most appropriate method is to define a set of static values for each option.
And there comes the Scala Object. Since Scala doesn't allow us to create static values and methods in a class, we need an object.
Let's define those five static values.

                                
    object StorageLevel{
        val DISK_ONLY = 0
        val MEMORY_ONLY = 1
        val MEMORY_ONLY_COMPRESSED = 2
        val MEMORY_AND_DISK = 3
        val MEMORY_AND_DISK_COMPRESSED = 4  
    }                                       
                            

Now, you should be able to instantiate the graph.

                                
    val g = new Graph("file_location")
    g.persist(0)
    g.persist(StorageLevel.MEMORY_ONLY)                                 
                            

I can call the persist method and instead of passing some meaningless integer values. I can pass a meaningful name.
However, I don't prefer to create a separate object named StorageLevel. That makes my life difficult. I may have to educate other developers and specifically tell them that I have defined those storage levels in a separate object.
My team would need to remember two different names. And there comes the companion object. I can define an object with the same name and keep it in the same source file.
Let's do that. If you are using REPL to compile these two constructs together, you must use the paste mode. The paste mode will compile them together and consider them part of the same source file. Great. Now I can use the same name.

                                
    val g = new Graph("file_location")
    g.persist(Graph.MEMORY_ONLY)                                        
                            

Apply method in Companions

Well, that's not the end of the companion object. Some people don't like using the new keyword to instantiate a class. Scala's philosophy is to help you create a concise code. Why are we typing new keyword then? Can you eliminate that?
The answer is yes, if you have a companion object, you can easily do that. Create an apply method. That's it.
Let me show you the new code. The code for the class remains same.

                                
    object Graph{
        val DISK_ONLY = 0
        val MEMORY_ONLY = 1
        val MEMORY_ONLY_COMPRESSED = 2
        val MEMORY_AND_DISK = 3
        val MEMORY_AND_DISK_COMPRESSED = 4   
        def apply(path:String) = new Graph(path)
    }                                           
                            

We simply add an apply method to the object. In this example, the apply method signature is a kind of replica of the constructor. Take the file location and all we do in the body is to wrap the new keyword within the apply method. Create a new instance and return it. Now, you can create a new Graph using the below code.

                                
    val g = Graph("file_location")                                       
                            

What are we doing? We are simply calling the apply method on the graph object, and the apply method returns a new instance of the class. However, we are still able to instantiate the graph using the new keyword.

                                
    val g = Graph("file_location")                                       
                            

For my example, there is no harm in allowing both methods to instantiate the graph. But if you want, you can disallow the new keyword. There are multiple ways to do that, but one simple method is to make a small change in the class definition. Make the constructor private. That's it. No one will be able to use the constructor.

                                
    class Graph private(path:String){
        println("Load the Graph from file")   
        def numEdges = 506
        def numVertices = 305
        def persist(storageLevel: Int) = println("Returns a new Graph")
    }
    object Graph{
        val DISK_ONLY = 0
        val MEMORY_ONLY = 1
        val MEMORY_ONLY_COMPRESSED = 2
        val MEMORY_AND_DISK = 3
        val MEMORY_AND_DISK_COMPRESSED = 4   
        def apply(path:String) = new Graph(path)
    }                                           
                            

However, the companion object can still use the private constructor. And that's one of the key advantages of companions. They can access the private methods of each other. I mean, a class can access the private methods of its companion object. And the object can also access the private methods of its companion class.
Amazing. Isn't it.

Apply method in Calss

I just want to make one more point about the Apply methods. As I mentioned earlier, you can define Apply method for a Class as well as for an Object. In our example, we defined apply method at the object. We can also define one more apply method at the class level. The purpose and the use of these two apply methods are different.
Let me explain it with an example.
Let's look at the Scala lists. We can create a Scala list using the below syntax.

                                
    val myList = List("India", "America", "Japan", "China")                                      
                            

Great. Now you understand that we are using a List object to instantiate a List class. Right? I can tell that because we are not using the new keyword and not even using a method name. At this place, we are calling the apply method of the list companion object. The apply method that we used here is an Apply method of the object. However, the list also has an Apply method of the class. Where do we use that one? The object level Apply method is static. So, we can use it directly. However, the class level Apply method is tied to an instance of the class. Once I have the instance, I can use the class level apply method. Like this.

                                
    myList(0)                                     
                            

The my-list is an instance of the List class. And I am using the apply method to get the element at index zero.
The point that I want to make is straightforward. Understand the difference between the two places of the Apply method and use them wisely.
Great. I talked about one purpose of having a companion object - Separation of concerns using a companion.
I will cover the Factory and builder pattern in the next video. See you again. Thank you for watching learning Journal. Keep learning and keep growing.


You will also like:


Kafka Core Concepts

Learn Apache Kafka core concepts and build a solid foundation on Apache Kafka.

Learning Journal

Pattern Matching

Scala takes the credit to bring pattern matching to the center.

Learning Journal

Apache Spark Introduction

What is Apache Spark and how it works? Learn Spark Architecture.

Learning Journal

Scala placeholder syntax

What is a scala placeholder syntax and why do we need it? Learn from experts.

Learning Journal

Higher Order functions

Scala allows you to create Higher Order functions as first class citizens.

Learning Journal