article banner (priority)

Effective Kotlin Item 33: 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:

class LinkedList<T>( val head: T, val tail: LinkedList<T>? ) val list = LinkedList(1, LinkedList(2, null))

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 vararg parameter.
  • From a collection of a different type, like List or Set.
  • 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, LinkedList 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 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 listOf from 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 List interface 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 null if the object cannot be created, like Connections.createOrNull(), which returns null when Connection 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.

  • 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 listOf, toList, 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

  • Builders

  • Conversion methods

  • Fake constructors

  • 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:

class LinkedList<T>( val head: T, val tail: LinkedList<T>? ) { companion object { fun <T> of(vararg elements: T): LinkedList<T> { /*...*/ } } } // Usage val list = LinkedList.of(1, 2)

The same can also be done with interfaces:

class LinkedList<T>( val head: T, val tail: LinkedList<T>? ) : MyList<T> { // ... } interface MyList<T> { // ... companion object { fun <T> of(vararg elements: T): MyList<T> { // ... } } } // Usage val list = MyList.of(1, 2)

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.

interface Tool { companion object { /*...*/ } } fun Tool.Companion.createBigTool(/*...*/): Tool { //... } val tool = Tool.createBigTool()

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 from and of, for example: val prime: BigInteger = BigInteger.valueOf(Integer.MAX_VALUE)

  • instance or 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)

  • createInstance or newInstance - Like getInstance, but this function guarantees that each call returns a new instance, for example: val newArray = Array.newInstance(classObject, arrayLen)

  • get{Type} - Like 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)

  • new{Type} - Like 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:

abstract class ActivityFactory { abstract fun getIntent(context: Context): Intent fun start(context: Context) { val intent = getIntent(context) context.startActivity(intent) } fun startForResult( activity: Activity, requestCode: Int ) { val intent = getIntent(activity) activity.startActivityForResult( intent, requestCode ) } } class MainActivity : AppCompatActivity() { //... companion object : ActivityFactory() { override fun getIntent(context: Context): Intent = Intent(context, MainActivity::class.java) } } // Usage val intent = MainActivity.getIntent(context) MainActivity.start(context) MainActivity.startForResult(activity, requestCode)

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, and 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:

class MainActivity : Activity { companion object { fun getIntent(context: Context) = Intent(context, MainActivity::class.java) } }

In the Kotlin Anko library, we can use the top-level function intentFor with a reified type instead:

intentFor<MainActivity>()

This function can also be used to pass arguments:

intentFor<MainActivity>("page" to 2, "row" to 10)

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:

val list = buildList { add(1) add(2) add(3) } println(list) // [1, 2, 3] val s = sequence { yield("A") yield("B") yield("C") } println(s.toList()) // [A, B, C]

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:

// Starting a coroutine scope.launch { val processes = repo.getActiveProcesses() for (process in processes) { launch { process.start() repo.markProcessAsDone(process.id) } } } // Defining a flow val flow = flow { var lastId: String = null do { val page = fetchPage(lastId) emit(page.data) lastId = page.lastId } while (!page.isLast) }

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.

val sequence: Sequence = list.asSequence() val double: Double = i.toDouble() val flow: Flow = observable.asFlow()

When we need to convert between UserJson and User in our applications, the most typical way is to define a conversion method as well. Such methods are often defined as extension functions.

class User( val id: UserId, val name: String, val surname: String, val age: Int, val tokens: List<Token> ) class UserJson( val id: UserId, val name: String, val surname: String, val age: Int, val tokens: List<Token> ) fun User.toUserJson() = UserJson( id = this.id, name = this.name, surname = this.surname, age = this.age, tokens = this.tokens ) fun UserJson.toUser() = User( id = this.id, name = this.name, surname = this.surname, age = this.age, tokens = this.tokens )

Copying methods

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 withSurname).

val user2 = user.copy() val user3 = user.withSurname(newSurname)

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:

class A fun b() = A() val a1 = A() val a2 = b()

They are also referenced in the same way as top-level functions (and constructor references implement function type):

val reference: () -> A = ::A

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:

List(4) { "User$it" } // [User0, User1, User2, User3]

This is why the following functions are included (since Kotlin 1.1) in the Kotlin stdlib:

public inline fun <T> List( size: Int, init: (index: Int) -> T ): List<T> = MutableList(size, init) public inline fun <T> MutableList( size: Int, init: (index: Int) -> T ): MutableList<T> { val list = ArrayList<T>(size) repeat(size) { index -> list.add(init(index)) } return list }

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:

class Tree<T> { companion object { operator fun <T> invoke( size: Int, generator: (Int) -> T ): Tree<T> { //... } } } // Usage Tree(10) { "$it" }

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:

Tree.invoke(10) { "$it" }

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:

data class Student( val id: Int, val name: String, val surname: String ) class StudentsFactory { var nextId = 0 fun next(name: String, surname: String) = Student(nextId++, name, surname) } val factory = StudentsFactory() val s1 = factory.next("Marcin", "Moskala") println(s1) // Student(id=0, name=Marcin, Surname=Moskala) val s2 = factory.next("Igor", "Wojda") println(s2) // Student(id=1, name=Igor, Surname=Wojda)

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.

class UserFactory( private val uuidProvider: UuidProvider, private val timeProvider: TimeProvider, private val tokenService: TokenService, ) { fun create(newUserData: NewUserData): User { val id = uuidProvider.next() return User( id = id, creationTime = timeProvider.now(), token = tokenService.generateToken(id), name = newUserData.name, surname = newUserData.surname, // ... ) } }

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. 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.

1:

See the section about primary/secondary constructors in the dictionary.

2:

This mechanism is better explained in my Kotlin Coroutines book.

3:

This will be explained soon in Item 35: Consider defining a DSL for complex object creation.

4:

Some might not consider conversion functions and copying functions to be factory functions, but I do, since they are alternatives to secondary constructors.

5:

Reified type parameters are explained in Item 48: Use inline modifier for functions with parameters of functional types.