1

Why not use top-level functions but overloaded invoke operators? Is there any advantage to overloading the invoke operator?

class GetFollowableTopicsUseCase @Inject constructor(
    private val topicsRepository: TopicsRepository,
    private val userDataRepository: UserDataRepository
) {

  operator fun invoke(sortBy: TopicSortField = NONE): Flow<List<FollowableTopic>> 
  ...

}
SageJustus
  • 631
  • 3
  • 9
  • 1
    How would you use top level functions instead of overloading the invoke operator in this case? – Sweeper Feb 13 '23 at 01:43
  • @Sweeper Use the code in `invoke()` as the body of the top-level function. – SageJustus Feb 13 '23 at 03:05
  • But where would that function get `topicsRepository` and `userDataRepository` from? They can be passed as parameters, but then you need to pass them on each call; here you construct (or get from the DI framework) a `GetFollowableTopicsUseCase` once to fix both. – Alexey Romanov Feb 24 '23 at 09:34
  • @FreedomFoolt Sorry, but just to be clear: do you mean the comment answers your question (and should be an answer), or that you already understood this? – Alexey Romanov Feb 24 '23 at 10:07
  • @AlexeyRomanov Yes, your comment is the answer to my question, the other answers are generic. After these few days, I have thought of this myself. By the way, I should have replied to one of your comments, but I can't see it. – SageJustus Feb 24 '23 at 15:56

3 Answers3

3

There's an old pair of sayings that floats around programming communities like these.

Closures are a poor man's objects, and objects are a poor man's closures.

The fact of the matter is that, in a sufficiently modern language like Kotlin (or like most languages that we use nowadays), objects and closures are pretty similar. You could replace every class in your Kotlin program with a mess of functions and mutable variables they close over. Likewise, you could replace every function in your program with an object that has an invoke function. But the former would be a constant wrestling match with the type system, and the latter would be absurdly verbose.

So Kotlin lets us do both. Which should you use? The advantage of functions is that they're short and snappy and to-the-point. And, to a functional programmer at least, functions should generally be free of side-effects. Objects, on the other hand, are loud and verbose. That's a bad thing, in that it takes longer to read and comprehend when skimming the code. But it's also a good thing, because it stops you from hiding complexity.

So if your function is simple, use a function. If it's complicated or stateful, use a named object and document it like any public class. As a few examples, here's how I would handle some different situations.

  • A function to add two numbers together is simple, side-effect-free, and referentially transparent. Use a function.
  • A function to add a number to a local val is still very simple. It's a closure, but the val is immutable, so the function's behavior is predictable. Using an object would be overkill, so make it a function.
  • A function that keeps track of how many times it's been called and prints out that number each time has side effects and local state. While it could be written as a fancy closure around a var, it would be better to make this a real object, whose counter variable is a genuine instance variable, so that anyone reading the code can see at a glance what's happening.
Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
1

In addition to Silvio's general answer, one specific case is for factory methods.

If you define a factory method like this:

class MyClass(…) {
    …
    companion object {
        operator fun invoke(…): MyClass = …
    }
}

Then calling the factory method looks exactly like calling a constructor: MyClass(…). This makes factory methods, with all their advantages, easier to use and hence more likely to be adopted.

(Obviously, this only makes sense when the parameter type(s) clearly distinguish the factory method from any public constructors, and also clearly indicate its purpose. In other cases, named factory methods are preferably.)

gidds
  • 16,558
  • 2
  • 19
  • 26
0

invoke can use properties and methods of the class it's defined in. If you try to make it a top-level function you would likely need to pass topicsRepository and userDataRepository as arguments, but you'd have to do so on every call. And if you need to calculate something from them which doesn't depend on the sortBy argument, you'd have to repeat the calculation each time (or use some workaround).

When invoke is a class method, you can construct (or get from the DI framework) a GetFollowableTopicsUseCase once to fix both and do any calculations which don't depend on sortBy in the constructor or lazy property initializers.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487