I’ll start by clarifying that we don’t avoid CompanionObject
s at all cost, we’ve just found a few scenarios where we’ve been able to remove them. Additionally, there are still use cases where they are very applicable.
In our Java code, we would commonly have things like the following:
private static final String SOME_VALUE = "foo"class LoggingHelper {
public static void logTheThing() {...}
}public class Foo {
public static void createFoo() {}
private String id;
public Foo(String id) { this.id = id; }
}
In the first case, this would likely be converted into a val
on a CompanionOjbect
of the containing class. In many cases this is the only reason a companion exists. We’ve felt this is using a companion unnecessarily because we can declare the value as a private const val
within the file. We’ve found this to be more readable and more immediately obvious.
In the case of LoggingHelper
this would actually be converted into an object
which may, or may not, be agreeable for your project. We’ve had a number of cases like this where we chose to use extension functions or top-level functions instead of keeping an object
class around. In the case of top-level functions, it removes the need to make a scoped call (from Kotlin) which we find syntactically more appealing.
In the last case, again the conversion tool will create a CompanionOjbect
. From Kotlin, we can use the method as Foo.createFoo()
like we are familiar from Java. From Java however, the syntax is more cumbersone: Foo.Companion.createFoo()
.
If we can write a top-level function to handle the same functionality then we can access it from Java as FooKt.createFoo()
. This looks a little more pleasant to us. It’s also not something we have to deal with too often as we move more towards Kotlin.
There are also some hidden costs associated with CompanionObjects
if things aren’t declared appropriately. I think this has played into our convention of avoiding companions when we can.
One thing I do want to point out is that while we’ve been able to use top-level functions pretty freely (and thereby avoid using CompanionObject
is some places) this approach doesn’t necessarily work for all projects.
With top-level functions, you run the risk of polluting the global namespace, whereas with CompanionObjects
you can leverage class scoping. When working on a large team, a library, or framework this scoping is likely much more important.