Written by Nihal Kashyap
Scala, being both an object-oriented and functional programming language, provides several powerful object-oriented features. In this article, we will discuss some of these features, including singleton objects and companion objects and classes.
Scope
The article explains singleton objects, companion class and companion object.
It explains the implementation of the Singleton design pattern using singleton objects, which is a popular object-oriented design pattern.
The article further explains how companion object can be used along with factory methods to create objects in a concise and expressive way.
Singleton objects
A Singleton object in Scala is a class that has only one instance. Since there can exist only a single instance of the class, they are known as singleton objects. They are globally accessible throughout the program. They are evaluated lazily i.e., when they are accessed or referenced for the first time. And subsequent references to the object will use the already existing instance.
What problem does it solve?
Many times we need only a single instance of a class throughout the codebase, such as a database connection or cache connection. The process of restricting object creation to a single instance is known as the singleton design pattern. In Scala, we can achieve it by using singleton objects.
Usage and application
Here is an example of a Singleton object that defines a cache connection.
object CacheConnection {
val host: String = "127.0.0.0"
val port: String = "11211"
}
A singleton object is created using the keyword object. It does not have a constructor. This is obvious because a constructor would return a new object every time it is called. The absence of a constructor ensures that only a single object is created.
Member variables and methods of a singleton object can be accessed by using the object name followed by the dot operator (`.`) and the member variable or method name. For example, we can access the host member variable as `CacheConnection.host`.
Companions
An object and a class sharing the same name and defined in the same source file are called companions. Companions can access each other's private fields and methods.
What problem does it solve?
Let's try to model the Citizen class of a country. The attributes of the Citizen class are:
name
dob
address
nationality
The `nationality` field is independent of an object or remains the same for all instances of the class. In many programming languages, most notably in Java, fields and methods of a class that are independent of the instances are known as static members.
Now, the problem is that all objects of the `Citizen` class for a particular country will have the same value for `nationality`. This repetition of the member variables also increases the memory usage.
To avoid this, we can segregate the static members and the non-static members into companion object and companion class, respectively.
Usage and application
class Citizen (val name: String, val dob: String, val address: String)
object Citizen {
val nationality: String = "Indian"
}
Now, only a single instance will contain the `nationality` variable.
The non-static variables i.e., `name`, `dob` and `address` reside in the class and the static variable i.e., `nationality` reside in the companion object.
val johnDoe: Citizen = new Citizen("John Doe", "12-06-1990", "Mumbai, Bandra")
println(johnDoe.name) // John Doe
println(Citizen.nationality) // Indian
The All-powerful apply() method
If objects contain the `apply()` method then, there is no need to explicitly invoke it. We can directly pass the parameters of the `apply()` method to the object.
Let's put an `apply()` method inside the `Citizen` class and try to access it using only an object of `Citizen`.
class Citizen (val name: String, val dob: String, val address: String) {
def apply(greetingMsg: String) = println(greetingMsg)
}
val johnDoe: Citizen = new Citizen("John Doe", "12-06-1990", "Mumbai, Bandra")
johnDoe("Hi, everyone!") // Hi, everyone!
Although, here we have invoked the `apply()` method by simply passing its parameters to the object, apart from that, the `apply()` method does nothing. The best application of the `apply()` method is to use it as a factory method.
Factory methods are methods that create objects of a class. They encapsulate the process of object creation and abstracts away the logic of object creation. In the following code example we will see how this is achieved.
We will define an `apply()` method in the `Citizen` companion object that will take the constructor parameters of the companion class as its parameters. The purpose of this `apply()` method is to create objects of the `Citizen` class.
class Citizen (val name: String, val dob: String, val address: String)
object Citizen {
val nationality: String = "Indian"
def apply(name: String, dob: String, address: String) = new Citizen(name, dob, address)
}
val johnDoe: Citizen = Citizen("John Doe", "12-06-1990", "Mumbai, Bandra")
We can see that in the above code snippet, the creation of objects just got easier by passing the constructor parameters to the companion object without using the `new` keyword. Thus, the use of the `apply()` method as a factory in a companion object allows us to create objects in a concise and expressive way.
Summary
Singleton objects allow us to create only a single instance of a class.
They are evaluated lazily, i.e., when accessed/ referenced for the first time.
Singleton objects allow us to implement the Singleton design pattern in a cleaner and fewer lines of code.
Companions are a pair of class and object sharing the same name and defined in the same file.
They can access each other's private member variables and methods.
An important application of companions is segregating static and non-static members into the Companion object and the Companion classes respectively, there by avoiding the creation of duplicate member variables across objects and reducing memory usage.
When the Companion object is used with the `apply()`method as a factory, it simplifies the creation of objects for its companion class.
Comments