Welcome back. In the earlier video, I talked about Scala Objects and two different type of their usage.
- Collection of Static methods
- 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.
- Class name and the Object name are same.
- 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.
- Separation of concerns using a Companion
- 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.
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.
Now, you should be able to instantiate the graph.
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.
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.
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.
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.
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.
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.
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.
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.