Does anyone else feel the need to post-fix `Flow&l...
# coroutines
d
Does anyone else feel the need to post-fix
Flow<T>
class members with
Flow
?
Copy code
class Server {
    val notificationsFlow: Flow<Notification>
    val notifications: Flow<Notification>
}
Do people prefer
notifications
or
notificationsFlow
?
same 1
a
notifications
βž• 8
d
Yeah I feel like I'm leaning that way now.
c
More importantly to stay consistent with the style in the codebase. There can be cases like migrating from
LiveData
to
Flow
that we may have both
notificationsLiveData
and
notificationsFlow
βž• 1
m
Code like the following:
Copy code
private val somethingMutableFlow = MutableStateFlow<Something>(Something.ZERO)
val somethingFlow : StateFlow<Something> get() = somethingMutableFlow
var something by somethingMutableFlow
    private set
is common for me.
e
Unless there's a similar wrapper (collection, flow, channel, etc.) of the same type nearby, I use
items
, not
itemsFlow
. If I have a mutable shared flow nearby, then that's called
mutableItems
next to
items
.
l
Same question, but with the type
Flow<List<Notification>>
🀯 1
πŸ€” 1
e
notificationLists
Note the s suffix, which indicates that this is something wrapping lists (wrapping notifications)
d
For this very specific notification case. I'd probably go with
notifications
still. It's all in the semantics I guess.
u
notificationGrid
πŸ˜„ 1
o
notificationBatches
In general I'd say: stay away from putting type names into variable names. Put the purpose there. Use type names as part of variable names as a last resort if they are the only differentiating factor (type conversion scenarios).
βž• 1
πŸ’― 1
☝️ 1
e
Sounds nice, but depending on the practice by the developers using with the code, it's not nice: they might have to check manually for the type (and therefore the interface they can use). Inferred type hints are available in IDEs, but not on e.g. GitHub. So when reviewing code on such a platform, it becomes more difficult to read. Besides, what does a 'batch' even mean other than it being some kind of group? What can I do with a batch? What functions does it have? Isn't that why we have well-established words like 'collection', 'set', 'flow', 'list', etc.? In this case, Kotlin's
List
interface clearly defines the contract:
A generic ordered collection of elements.
Sound like a good description of the the use case to me: there's notifications and they're ordered. No need for a new word like 'batch'. What if a future type
Batch
is introduced in your code base. Then you'd have to remember that
notificationBatches
is actually not a good name any more. Admittedly, the same applies to the
notificationLists
name if you'd ever refactor to
Flow<Collection<Notification>>
or something, but that's a refactor directly on this variable's type, so easier to see/remember. It's just the nature of refactoring. So I stay by
notificationLists
(if you're using and expecting flows by default) or even
notificationListFlow
. There's a strong type system for a reason: use types explicitly, either by specifying them, or using them in the name if you rely on type inference.
o
Using the wrong tools for development is not an excuse I would easily accept these days. Indeed, the type system is there for a reason and its namespace is deliberately separate from that of variables. Mix the two and you'd end up with https://en.wikipedia.org/wiki/Hungarian_notation. Batch in this case means exactly that: A bunch of notifications, no strings attached. This is a contract with a sufficient, but least specific promise. Why imply ordering? How do you know that's important for the use case at hand? Maybe it's just an implementation detail, maybe an array would do or some other
Iterable
? Once you're exposing too much, people will depend on it, limiting your refactoring options.
e
Why imply ordering?
The question was how to name
Flow<List<Notification>>
, so the
List
type and therefore the ordering of elements was imposed, not implied.
If that's not the intended behaviour, then use a
Collection
or another wrapper type
Honestly, but that might be my language barrier (I'm dutch, a non-native english speaker) I don't understand what batch means other than what a word like collection, set or list already describes very well (at least on JVM)
So for me the contract of batch is completely undefined, whereas the contract of a list is perfectly well defined
In any case, let's agree that naming is hard, because there are many other cases we don't even want to discuss πŸ˜›
o
I'm a non-native English speaker, too (German). Your English might even be better than mine (in many cases Dutch people beat Germans easily in that discipline πŸ‘). In this case, we just cannot know if
List
is part of the implementation or the use-site contract, as all we have is a simple variable declaration. So with respect to naming, I went with as little implied coupling as possible. "Batch" has a lesser meaning. While in many cases that would not be a good thing, in this case it's exactly what I intended. πŸ™‚
e
Oh hi neighbour! waving from afar left I still think that in idiomatic Kotlin, as it's a high level language, it pays to use types and their contracts to model (intended) behaviour/usage. So if you use a list, you intend the list interface to be available to the users. I think you said something similar in your previous comment? So I guess we agree about that. The question is: if the type, which holds (intended) behaviour information, isn't clearly visible (e.g. inferred), then something else must tell the user (that is you and your fellow code readers, not the compiler in the case of Kotlin) about the intended usage. Therefore the name could (I'm not saying should) reflect the type. Yes, that can lead to Hungarian notation. But what else than a flow of lists of notifications is the type in question? If you want it to behave and be used as a notification batch, then create a type (e.g. interface or alias)
NotificationBatch
or something similar. Then you can perfectly name the variable like you suggested! (And that would still be Hungarian notation πŸ˜‰)
o
Well I'd say "Hungarian" would be to encode everything and the kitchen sink into the name. And yes, if
List
is what describes your purpose best, then go with
notificationLists
. Once we had really figured out the true intention of the example, we might actually agree on naming! πŸ˜„
πŸ‘ 1
u
I have this dillemma with my Dao's when I have a
val foo: Flow<Foo>
but also a suspend function which selects foo 1 time as normal.. how to call this..
loadFoo()
,
findFoo()
? or other way around, call the flow
fooFlow
and
foo()
for the suspend function?
e
My two cents: DAOs basically expose CRUD db operations, so what about using verbs like create, read, update and delete? And then names for some cases: -
suspend fun readFoo(): Foo
-
fun readFoos(): Flow<Foo>
And otherwise I don't think it's too bad to just name them
fooFlow
and
foo
. Or maybe even name the flow just
foos
.
o
And another 2 cents: Do not use
loadFoo()
,
findFoo()
,
readFoo()
as these names would imply something imperative, not returning a value. So go with
private val _foo: Flow<Foo>
and
suspend fun foo(): Foo
. See Choose good names, an underestimated chapter in the Kotlin coding conventions. I know this case is less than ideal and a suspending
get()
looks preferable, but its not there yet. That a function and property exist side-by-side, serving the same purpose, is really just an implementation artifact. There is no perfect naming solution to overcome this. There should ideally be just one thing.
u
ive only seen underscore in private vals, youd put them public? i'd find that odd
o
No, I would not. See above:
private val _foo: Flow<Foo>
If you really need both in public, the accessor function and the variable, I would reconsider the design. While I would not blindly adapt the design for the sake of naming, in my experience problematic designs often manifest themselves in naming difficulties. Your mileage may vary, but that's what I would look at.
u
Well, can you add your opinion? Its a Dao wrapping database, contains ways to read and write data + some queries are observable (flows), how would you design it?
o
I guess I'd need more details about your use case for that. Can you point to a GitHub gist or something?
u
something like this
Copy code
interface FooDao {
	fun foos(): Flow<List<Foo>>

	fun fooByIdFlow(id: Strong): Flow<Foo?>

	suspend fun fooById(id: String) : Foo?

	suspend fun saveFoos(foos: List<Foo>)
}
and its implementation is driven by sqldelight, for example
o
DAOs are less than ideal in general, so I'd avoid that approach. I'd probably go with something lightweight-ORM-like, otherwise I might lean towards something like the following and use one abstraction for the storage (database) subsystem. In the end, it would all depend on the use-site needs, number of classes involved, and so on, but hopefully you get the idea:
Copy code
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*

class Foo(id: String, name: String)

sealed class FooCriteria {
    object All : FooCriteria()
    class Id(value: String) : FooCriteria()
    class Name(value: String) : FooCriteria()
}

interface Storage {
    fun foos(criteria: FooCriteria): Flow<Foo>
    suspend fun addOrUpdate(foos: Iterable<Foo>)
}

fun storage(): Storage = TODO()

fun main() = runBlocking {
    storage().foos(FooCriteria.All).collect {
        println(it)
    }

    storage().foos(FooCriteria.Id("#23")).collect {
        println(it)
    }
}
u
Hmm, I don't follow. You'd rather spread ORM imports all over the code, rather than hiding the ORM behind the DAO interface? Also, looking at the sample, that looks like my Dao just renamed to Storage, unless im missing something. Or is the trick in the Criteria abstraction?
o
Storage
in this case is your entire database, not a single-object's persistence API. As the above interface does not even have to be a database (could be flat file or whatever), it is named just
Storage
. There is really no special trick involved. In the end it is more about choosing the "right" abstraction. A DAO is just a name for a bunch of functions without clear boundaries. Storage (or, more specific, database) is a better understood abstraction (you can almost instantly visualize it). And yes, I would not care spreading ORM imports, that would not be a factor if the ORM is the proper abstraction. Keep in mind that all of this is mostly about how you think about stuff, how easily you can understand it initially and then pick it up again after a 6 month pause. The perception also changes depending on what you are used to. You may not follow if you were used to a different set of abstractions. In that case, the quality would show if you try to introduce those abstractions to new people, unfamiliar with the chosen concepts.