Effective Kotlin Item 33: Consider factory functions instead of secondary constructors
To create an object from a class, you need to use a constructor. In Kotlin, it is typically the primary constructor1:
Primary constructors usually include mainly or only properties; as a result, primary constructor parameters are strongly bound to object structure. This is enough for simple objects, but more complex objects 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
- From a collection of a different type, like
- 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
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 size of the list? It is definitely not self-explanatory. In such a situation, a name, like
ArrayList.withSize(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
listOffrom stdlib. Its declared return type is
List, 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 the
Listinterface and act the same way, everything will be fine.
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
nullif the object cannot be created, like
Connections.createOrNull(), which returns
Connectioncannot 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 (
privatemodifier) or in the same module (
Factory functions can be inlined, so their type parameters can be reified5.
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.
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 secondary constructor. Practically all classes have one constructor, and we create most objects using different kinds of factory functions. We create a list with
List, a fake constructor, etc. These are different kinds of factory functions. We should do the same when we design our class creation. For that, it is good to know the most important kinds of factory functions and their conventions:
Companion object factory functions
Top-level factory functions
Methods on 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 to
of, for example:
val prime: BigInteger = BigInteger.valueOf(Integer.MAX_VALUE)
getInstance- 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)
getInstance, but this function guarantees that each call returns a new instance, for example:
val newArray = Array.newInstance(classObject, arrayLen)
getInstance, 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)
newInstance, 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)
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
mapOf. Similarly, library designers specify top-level functions that are used to create objects. Top-level factory functions are used widely. For example, in Android, we have the tradition of defining a function to create an
Intent to start an Activity. In Kotlin, the
getIntent() can be written as a companion object function:
In the Kotlin Anko library, we can use the top-level function
intentFor with a reified type instead:
This function can also be used to pass arguments:
Object creation using top-level functions is a perfect choice for small and commonly created objects like
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.
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 often convert from one type to another. You might convert from
Double, from RxJava
Flow, etc. For all these, the standard way is to use conversion methods.
When we need to convert between
User in our applications, the most typical way is to define a conversion method as well. Such methods are often defined as extension functions.
When you need to make a copy of an object, define a copying method instead of defining a copying constructor4. 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
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.
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 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.
MutableList are interfaces. They cannot have constructors, but Kotlin developers wanted to allow the following
This is why the following functions are included (since Kotlin 1.1) 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.
The two main reasons why developers choose fake constructors over real ones are:
- To have a "constructor" for an interface
- To have reified type parameters
Otherwise, fake constructors should behave like normal constructors. They look like constructors and they should behave like constructors. If you want to include caching or return a nullable type or a subclass of a class that can be created, consider using a factory function with a name, like a companion object factory method.
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 very rarely done. I do not recommend it, primarily because it violates Item 12: Use operator methods according to their names. 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:
val f: ()->Tree = ::Tree
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.
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. Some of them should preferably be used with caution, such as Fake Constructors or Top-Level Factory Methods. The most universal way to define a factory function is by using a Companion Object, which is safe and very intuitive for most developers. Its usage is very similar to Java Static Factory Methods, and Kotlin mainly inherits its style and practices from Java.
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 35: Consider defining a DSL for complex object creation.
Some might not consider conversion functions and copying functions to be factory functions, but I do, since they are alternatives to secondary constructors.
Reified type parameters are explained in Item 48: Use inline modifier for functions with parameters of functional types.