I love Kotlin but I’m not quite sure how to properly separate concepts.
To give concrete examples:
Default enum value
You refer to enum values like Enum.Value1, how do you get a default value? It’d make sense to have something like Enum.default, so I write:
enum class Enum {
Value1,
Value2;
companion object {
val default = Value2
}
}
This creates a whole new object that I don’t need and according to other discussions I shouldn’t be using objects for namespacing. The alternative would be a top-level EnumDefault I guess?
Factory methods
Similarly, there are factory methods such as Url.fromString() that I either have to put into companion object or define at top level.
Global constants and variables
It’s awesome when you can keep them private by sometimes you have various constants that just need to be public. And sometimes they are closely related. For instance, you could have BUTTON_ANIMATION_DURATION and BUTTON_ANIMATION_DELAY. It would be nice if you didn’t have to define it like that, instead, you might want to say something like
object ButtonAnimation {
const val DURATION = 250
const val DELAY = 100
}
but then again, you created an object you don’t need, and unless you are using const accessing DURATION has to go through the whole accessing ButtonAnimation.INSTANCE and calling ButtonAnimation.getDURATION()…
Nested classes
In a similar way, you might want to have a file with classes such as Request and Response that are accesses as Handshake.Request and Handshake.Response from outside. You can somewhat solve this by putting them into another class, if it makes sense, or an interface.
It feels that it would be useful to allow importing packages. This would also help with conflicting import names. For instance, instead of writing
import library.widget.Preference as WidgetPreferenece
import myapp.config.Preference as ConfigPreference
you could write
import library.widget as widget
import myapp.config as config
val foo = findView<widget.Preference>()
val bar = config.Preference()
Also it would perhaps be nice to be able to write namespace ButtonAnimation {} or something like the following, that would be used solely for keeping constants, factory methods, etc together:
enum class Enum {
Value1,
Value2;
companion namespace { // not an object!
val default = Value2
}
}
Always using top-level is a terrible alternative to using object for namespaces imo. If you have a large amount of top-level items things can get out of hand. Using objects for namespaces gives you structure.
It is a sore spot in Kotlin. There should be a namespace keyword, but in the mean time I would use the object keyword. Unless you’re using object to make thousands upon thousands of namespaces, the overhead from all the unnecessary objects being created is negligible.
But rather than a namespace keyword, if this is possible, it would be nice if the compiler could detect that your object is only being used as a namespace and not generate unnecessary objects under the hood.
I’ve never found using objects for namespaces to create a problem. We use it quite heavily for typesafe dependency management.
I do think that the awkward the Op is seeing above is because of an unintentional misuse of the Enum itself. The Enum class is for detailing all possible values, things like default values are context-specific business logic in many systems, so for that reason we wouldn’t allow the above.
I recommend keeping business logic in specialised service classes, which aids testability.
Alternatively in Kotlin I believe you could look at using an Interface to declare a default enum
That depends on the Enum in question imo. I use Enums a lot for stuff like preference files or network protocols so I also keep factory methods inside them (e.g. Compression.fromString("zlib") → Compression.Zlib) and so default values go there as well. Perhaps you can argue that this stuff should be elsewhere but I find this kind of organization much easier to read
If you have a look at the literature for the design of enums, in various languages, you’ll find clear patterns.
Nowhere in the Java documentation, or the Kotlin documentation, will you find examples of them being used for business logic like default values.
You can do it, but its not considered a good idea.
If using for any sort of configuration, when writing larger systems in Kotlin, I recommend adopting “DomainPreferences” classes, eg “EmailPreferences” to wrap groups of values, since they keep things like defaults and such in a single place, and they make it easy to test dependant services, since you can just feed them a POJO - what do we call POJOs in Kotlin anyway…?
Here’s a more or less real class that’s a part of a handhshake code
enum class Compression(val string: String) {
Off("off"),
Zlib("zlib");
companion object {
val default = Off
fun fromString(string: String) = Compression::string.find(string)
?: throw IllegalArgumentException("Bad compression method: '$string'")
}
}
In my opinion it clearly describes the possible values, the possible string representation of those, what to do when the representation is missing and the way to convert it to a value.
If I have EmailPreferences then where do I put the default value for compression? Something like this?
object EmailPreferences {
enum class Compression { ... }
val defaultCompression = ...
fun compressionFromString(...) = ...
}
Or something like this?
object EmailPreferences {
object Compression {
enum class Compression { ... }
val default = ...
fun fromString(...) = ...
}
}
I find both of these awful
Nowhere in the Java documentation, or the Kotlin documentation, will you find examples of them being used for business logic like default values.
I don’t think I’ve seen examples of this in there but then I haven’t seen it elsewhere in the documentation either
I’ve put together a very contrived example, but its more to do with the fact that things like ‘defaults’ for use by a system, are often derived from something else.
Depending on the frameworks, some of that info isn’t available until runtime, hence avoiding Objects. We build a lot of microservices on Ktor for example, and the Preference classes we build load the overall config, validate or default their particular domains, report any problems, convert values etc.
I see your point, but in your example defaultAlgorithm is not really “default” in the sense that CompressionPreferences can’t provide a non-default algorithm, nor it would make sense for it to provide one. So it should be just algorithm and of course you wouldn’t put it inside the enum.
Where you would use Compression.default would be something like
val compressionString = protocolMessage.getString("compression", null)
val compression = if (compressionString == null) {
Compression.fromString(compressionString)
} else {
Compression.default
}
Or you could make it private and call Compression.fromStringOrNull(...)