Idiomatic way for asynchronous construction

I have a class that requires non-trivial initialization. Based on its constructor arguments, it performs some potentially long-running operations (complex calculations, hard disk access…), then finally initializes its private properties with the result.

Here’s a sketch:

class MyClass(publicParam: Int) {
    private val implementationDetail1: Int
    private val implementationDetail2: String
    
    init {
        // Long-running initialization code
        implementationDetail1 = 42;
        implementationDetail2 = "Liff"
    }
    
    // ... public code
}

As long as the initialization code is synchronous, this approach works. (Although it’s debatable whether long-running constructors are good style.) But I’d like to rewrite the initialization logic to use suspend functions. Constructors can’t be suspend functions, so I’ll have to restructure the code.

Here’s my first approach, using a companion object:

class MyClass private constructor(
    private val implementationDetail1: Int,
    private val implementationDetail2: String
) {
	companion object {
        suspend fun create(publicParam: Int): MyClass {
            // ...long-running initialization code...
            val implementationDetail1 = 42;
            val implementationDetail2 = "Liff"
            return MyClass(implementationDetail1, implementationDetail2)
        }
    }
    
    // ...public code...
}

On the one hand, I like that all the initialization logic still resides within the class. On the other hand, MyClass.create() is a static function, and static functions seem to be frowned upon in Kotlin.

Which brings me to my second approach:

suspend fun makeMyClass(publicParam: Int): MyClass {
    // ...long-running initialization code...
    val implementationDetail1 = 42;
    val implementationDetail2 = "Liff"
    return MyClass(implementationDetail1, implementationDetail2)
}

class MyClass(
    private val implementationDetail1: Int,
    private val implementationDetail2: String
) {
    // ...public code...
}

I’ve moved the initialization logic into a top-level factory function. This seems to be the standard approach in Kotlin, and it avoids the static function. The big problem with this approach is that the private properties of MyClass have suddenly become part of its public constructor. They are no longer an implementation detail of the class, but visible from the outside. And initialization logic that closely belongs to the class has been moved to an outside function.

So I wonder: What is the idiomatic approach for asynchronous construction? Is there a better way than what I came up with?

I didn’t found nothing about best practice for your use case.
I solved a similar issue using a suspending factory.

However I left you a tip, it looks idiomatic

class MyClass private constructor(private val string: String) {

    companion object {
        suspend operator fun invoke(n: Int) = MyClass(n.toString())
    }
}

suspend fun main() {
    val c = MyClass(42) // a.k.a. MyClass.Companion.invoke(42)
}
2 Likes

Wow! Using an operator to fake a constructor signature. That’s certainly clever!

I haven’t decided yet whether to regard it as good style or bad (principle of least surprise, you know), but it’s certainly a great idea. Thanks!

1 Like

You do not have static functions in your code, because Kotlin does not have static functions. You can make functions static using @JvmStatic if you need this for interoperability with other JVM languages. The creation function in the companion object of your class is an instance function of that companion object (which is the singleton instance of the automatically generated companion class).

I really do not see a problem with keeping the creation functions of your class with your class (which, in Kotlin, means in the companion object), unless there are a multitude of ways in which an instance of the class can be created.

Another advantage of using a function in a companion object is that you can make the constructor private to prevent incorrect initialization. But I am not sure if this is desirable in this case. If the long-running initialization can be moved out of the constructor to a creation function, then the initialization is not part of the core functionality of the class. You might want to initialize an instance differently in the future. But switching from a private to a public constructor can be done without breaking existing code when the need arises.

I would use the following rules to determine where to put the initialization:

  • The class may only be initialized in 1 way now and in the future: function in the companion object and a private constructor.
  • Otherwise: top-level function (with a descriptive name) and a public constructor.

@jstuyts In my case, the initialization code clearly belongs to MyClass. That’s what I wanted to express by naming the properties implementationDetail1 and implementationDetail2: Exposing these values as the arguments of a public constructor would break encapsulation.

I believe I’m going to use the companion object, as suggested by both @fvasco and @jstuyts. I still haven’t decided about the clever call operator, though.

I seem to remember that, in some other big thread about whether Kotlin should have static find like other languages, some (from JetBrains?) said that factory methods like this are exactly the sort of thing you want to put into companion objects (because of access to private bits of the class) whereas top-level functions are better for everything else. That’s second-hand advice but it sort of makes sense to me.

2 Likes