Effective Kotlin Item 32: Consider factory functions instead of secondary constructors
This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.
To create an object from a class, you need to use a constructor. In Kotlin, it is typically the primary constructor1:
It is typical of Kotlin classes that their primary constructor defines properties that are essential part of this object state; as a result, primary constructor parameters are strongly bound to object structure. Using primary constructor for object creation is enough for simple classes, but more complex cases require different ways of constructing them. Think of the LinkedList
from the above snippet. We might want to create it:
- Based on a set of items passed with the
vararg
parameter. - From a collection of a different type, like
List
orSet
. - From another instance of the same type.
It is poor practice to define such functions as constructors. Don't do that. It is better to define them as functions (like linkedListOf
, toLinkedList
or copy
), and here are a few reasons why:
Unlike constructors, functions have names. Names explain how an object is created and what the arguments are. For example, let’s say that you see the following code:
ArrayList(3)
. Can you guess what the argument means? Is it supposed to be the first element in the newly created list, or is it the initial capacity of the list? It is definitely not self-explanatory. In such a situation, a name likeArrayList.withCapacity(3)
would clear up any confusion. Names are really useful: they explain arguments or characteristic ways of object creation. Another reason to have a name is that it solves potential conflicts between constructors with the same parameter types.Unlike constructors, functions can return an object of any subtype of their return type. This is especially important when we want to hide actual object implementations behind an interface. Think of
listOf
from stdlib. Its declared return type isList
, which is an interface. But what does this really return? The answer depends on the platform we use. It is different for Kotlin/JVM, Kotlin/JS, and Kotlin/Native because they each use different built-in collections. This is an important optimization that was implemented by the Kotlin team. It also gives Kotlin creators much more freedom. The actual type of a list might change over time, but as long as new objects still implement theList
interface and act the same way, everything will be fine. Another example islazy
that declaresLazy
interface as its result type, and depending on thead safety mode, in JVM it returns eitherSynchronizedLazyImpl
,SafePublicationLazyImpl
orUnsafeLazyImpl
. Each of those classes is private, so their implementations are protected.Unlike constructors, functions are not required to create a new object each time they’re invoked. This can be helpful because when we create objects using functions, we can include a caching mechanism to optimize object creation or to ensure object reuse for some cases (like in the Singleton pattern). We can also define a static factory function that returns
null
if the object cannot be created, likeConnections.createOrNull()
, which returnsnull
whenConnection
cannot be created for some reason.Factory functions can provide objects that might not yet exist. This is intensively used by creators of libraries that are based on annotation processing. In this way, programmers can operate on objects that will be generated or used via a proxy without building the project.
When we define a factory function outside an object, we can control its visibility. For instance, we can make a top-level factory function accessible only in the same file (
private
modifier) or in the same module (internal
modifier).Factory functions can be inlined, so their type parameters can be reified5. Libraries use this to provide a more convenient API.
A constructor needs to immediately call a constructor of a superclass or a primary constructor. When we use factory functions, we can postpone constructor usage. That allows us to include a more complex algorithm in object creation.
Functions used to create an object are called factory functions. They are very important in Kotlin. When you search through Kotlin’s official libraries, including the standard library, you will have trouble finding a non-private constructor, not to mention a secondary constructor. Most libraries expose only factory functions, most applications expose only primary constructors.
There are many kinds of factory functions we can use. We can create a list with listOf
, toList
, List
, etc. Those are all factory functions. Let's learn about the most important kinds of factory functions and their conventions:
Companion object factory functions
Top-level factory functions
Builders
Conversion methods
Copying methods
Fake constructors
Methods in factory classes
Companion Object Factory Functions
In Java, every function has to be placed in a class. This is why most factory functions in Java are static functions that are placed either in the class they are producing or in some accumulator of static functions (like Files
). Since the majority of the Kotlin community originated in Java, it has become popular to mimic this practice by defining factory functions in companion objects:
The same can also be done with interfaces:
The advantage of this practice is that it is widely recognized among different programming languages. In some languages, like C++, it is called a Named Constructor Idiom as its usage is similar to a constructor, but with a name. It is also highly interoperable with other languages. From my personal experience, we used companion object factory functions most often when we were writing tests in Groovy. You just need to use JvmStatic
annotation before the function, and you can easily use such a function in Groovy or Java in the same way as you use it in Kotlin.
The disadvantage of this practice is its complexity. Writing List.of
is longer than listOf
because it requires applying a suggestion two times instead of one. A companion object factory function needs to be defined in a companion object, while a top-level function can be defined anywhere.
It is worth mentioning that a companion object factory function can be defined as an extension to a companion object. It is possible to define an extension function to a companion object as long as such an object (even an empty one) exists.
There are some naming conventions for companion object factory functions. They are generally a Java legacy, but they still seem to be alive in our community:
from
- A type-conversion function that expects a single argument and returns a corresponding instance of the same type, for example:val date: Date = Date.from(instant)
of
- An aggregation function that takes multiple arguments and returns an instance of the same type that incorporates them, for example:val faceCards: Set<Rank> = EnumSet.of(JACK, QUEEN, KING)
valueOf
- A more verbose alternative tofrom
andof
, for example:val prime: BigInteger = BigInteger.valueOf(Integer.MAX_VALUE)
instance
orgetInstance
- Used in singletons to get the object instance. When parameterized, it will return an instance parameterized by arguments. Often, we can expect the returned instance to always be the same when the arguments are the same, for example:val luke: StackWalker = StackWalker.getInstance(options)
createInstance
ornewInstance
- LikegetInstance
, but this function guarantees that each call returns a new instance, for example:val newArray = Array.newInstance(classObject, arrayLen)
get{Type}
- LikegetInstance
, but used if the factory function is in a different class. Type is the type of the object returned by the factory function, for example:val fs: FileStore = Files.getFileStore(path)
new{Type}
- LikenewInstance
, but used if the factory function is in a different class. Type is the type of object returned by the factory function, for example:val br: BufferedReader = Files.newBufferedReader(path)
Those conventions map to other kind of factory functions. For example, listOf
suggests that is creates a list from a set of elements, so it is an aggregation function. createViewModel
suggests that it creates a new instance of a ViewModel, so it is a new{Type}
function.
Companion objects are often treated as an alternative to static elements, but they are much more than that. Companion objects can implement interfaces and extend classes. This is a response to a popular request to allow inheritance for "static" elements. You can create abstract builders that are extended by concrete companion objects:
Notice that such abstract companion object factories can hold values, and so they can implement caching or support fake creation for testing. The advantages of companion objects are not as well used as they could be in the Kotlin programming community. Still, if you look at the implementations of the Kotlin team’s libraries, you will see that companion objects are used extensively. For instance, in the Kotlin Coroutines library, nearly every companion object of a coroutine context implements a CoroutineContext.Key
interface, which serves as a key we use to identify this context2.
Top-level factory functions
A popular way to create an object is by using top-level factory functions. Some common examples are listOf
, setOf
, mapOf
, lazy
, sequence
, flow
, etc.
Top-level factory functions are also used in projects to create objects in the way specific to the project. For instance, this is how one project could define Retrofit service creation:
Object creation using top-level functions is a perfect choice for small and commonly created objects like List
or Map
because listOf(1,2,3)
is simpler and more readable than List.of(1,2,3)
. However, public top-level functions need to be used judiciously. Public top-level functions have a disadvantage: they are available everywhere, therefore it is easy to clutter up the developer’s IDE tips. This problem becomes more serious when top-level functions have the same names as class methods and therefore get confused with them. This is why top-level functions should be named wisely.
Builders
A very important kind of top-level factory function is builders. A good example is a list or a sequence builder:
The typical way to implement a builder in Kotlin is using a top-level function and a DSL pattern3. In Kotlin Coroutines, builders are the standard way to start a coroutine or define a flow:
We will discuss DSLs in detail in Item 34: Consider defining a DSL for complex object creation. Of course, you can also meet builders defined using Java Builder Pattern, that look like this:
In Kotlin we consider them less idiomatic than DSL builders. However, they are still used in some libraries.
Conversion methods
We often convert from one type to another. You might convert from List
to Sequence
, from Int
to Double
, from RxJava Observable
to Flow
, etc. For all these, the standard way is to use conversion methods. Conversion methods are methods used to convert from one type to another. They are typically named to{Type}
or as{Type}
. For example:
The to
prefix means that we are actually creating a new object of another type. For instance, if you call toList
on a Sequence
, you will get a new List
object, which means that all elements of the new list are calculated and accumulated into a newly created list when this function is called. The as
prefix means that the newly created object is a wrapper or an extracted part of the original object. For example, if you call asSequence
on a List
, the result object will be a wrapper around the original list. Using as
conversion functions is more efficient but can lead to synchronization problems or unexpected behavior. For example, if you call asSequence
on a MutableList
, you will get a Sequence
that references the original list.
We often define our own conversion functions to convert between our own types. For example, when we need to convert between UserJson
and User
in an example application. Such methods are often defined as extension functions.
Copying methods
When you need to make a copy of an object, define a copying method instead of defining a copying constructor. When you just want to make a direct copy, a good name is copy
. When you need to apply a change to this object, a good name starts with with
and the name of the property that should be changed (like withSurname
).
Data classes support the copy
method, which can modify any primary constructor property, as we will see in Item 37: Use the data modifier to represent a bundle of data.
Fake constructors
Constructors in Kotlin are used the same way as top-level functions:
They are also referenced in the same way as top-level functions (and constructor references implement a function type):
From a usage point of view, capitalization is the only distinction between constructors and functions. By convention, classes begin with an uppercase letter, and functions begin with a lowercase letter. However, technically, functions can begin with an uppercase letter. This is used in different places, for example, in the Kotlin standard library. List
and MutableList
are interfaces. They cannot have constructors, but Kotlin developers wanted to allow the following List
construction:
This is why the following functions are included in the Kotlin stdlib:
These top-level functions look and act like constructors, but they have all the advantages of factory functions. Lots of developers are unaware of the fact that they are top-level functions under the hood. This is why they are often called fake constructors. They are a specific kind of top-level factory functions.
It is a very popular pattern in Kotlin libraries to expose only an interface, and produce its instance using a fake constructor. This way the actual implementation can be hidden. That has all advantages of factory functions, like:
- Hiding the actual implementation behind an interface (see
Job
,CoroutineScope
,Mutex
from kotlinx.coroutines). - Depending on arguments, a different implementation can be returned, optimized for the given case (see
Channel
from kotlinx.coroutines). - An algorithm can be used to create an object, which is not possible with a constructor (see
List
andMutableList
).
Here are examples of fake constructors from the Kotlin Coroutines library:
Fake constructors should conceptually behave like regular constructors, otherwise you should prefer a different factory function kind.
There is one more way to declare a fake constructor. A similar result can be achieved using a companion object with the invoke
operator. Take a look at the following example:
However, implementing invoke in a companion object to make a fake constructor is considered less idiomatic. I do not recommend it, primarily because it violates Item 11: An operator’s meaning should be consistent with its function name. What does it mean to invoke a companion object? Remember that the name can be used instead of the operator:
Invocation is a different operation from object construction. Using the invoke
operator in this way is inconsistent with its name. More importantly, this approach is more complicated than just a top-level function. Just compare what reflection looks like when we reference a constructor, a fake constructor, and the invoke
function in a companion object:
Constructor:
val f: ()->Tree = ::Tree
Fake constructor:
val f: ()->Tree = ::Tree
Invoke in a companion object:
val f: ()->Tree = Tree.Companion::invoke
I recommend using standard top-level functions when you need a fake constructor. However, these should be used sparingly to suggest typical constructor-like usage when we cannot define a constructor in the class itself, or when we need a capability that constructors do not offer (like a reified type parameter).
Methods on factory classes
There are many creational patterns associated with factory classes. For instance, an abstract factory or a prototype. Every creational pattern has some advantages.
Factory classes hold advantages over factory functions because classes can have a state. For instance, this is a very simple factory class that produces students with sequential id numbers:
Factory classes can have properties that can be used to optimize object creation. When we can hold a state, we can introduce different kinds of optimizations or capabilities. We can, for instance, use caching or speed up object creation by duplicating previously created objects.
In practice, we most often extract factory classes when object creation requires multiple services or repositories. Extracting object creation logic helps us better organize our code.
Summary
As you can see, Kotlin offers a variety of ways to specify factory functions, and they all have their own use. We should have them in mind when we design object creation. Each of them is reasonable for different cases. The most important thing is to be aware of the differences between them and to use them appropriately. The most popular factory function types are:
- Companion object factory functions
- Top-level factory functions (including fake constructors and builders)
- Conversion functions
- Methods on factory classes
See the section about primary/secondary constructors in the dictionary.
This mechanism is better explained in my Kotlin Coroutines book.
This will be explained soon in Item 34: Consider defining a DSL for complex object creation.
Reified type parameters are explained in Item 51: Use the inline modifier for functions with parameters of functional types.