+ This presumes SELECT * projections..How about pa...
# android-architecture
u
+ This presumes SELECT * projections..How about partial projections? Would you rather have scoped object of only user name and imagr url? Or full User object with name and url filled and rest of fields null?
g
normally yes, data, domain and ui models and mappers between them. Scoped objects for partial projections.
u
hmm not sure If I get you. I you have a domain object Foo, how is it then mapped from partial partialDbFoo? are the rest of fields null?
d
MessageDTO come from a network call. It can then be mapped into a CachedMessage object to be stored into the DB. The CachedMessage can then be mapped into a DomainMessage if some domain related things need to be done with it. Alternatively the CachedMessage can be mapped to a RenderableMessage for the UI to consume. Thinking of each part of your application as separate modules with different concerns helps.
u
@dewildte this is all clear and good. Where it gets problematic for me is 1) joins 2) partial projections of selects
d
Ok, why exactly?
u
which implies the domain object needs to either have variations, or nullable fields
or do select * everyhere, no?
imma be concrete so we dont just ramble, I have accounts, each account has channels, each channel has messages (like slack)
d
ok
u
so when I display messages in channel ui; the select is basically messages join channel
- when I display notification I need account too; so message join channel join account where message id = ?
so; how would your domain object for this look? nullable account?
d
Why can't the messages objects contain the channel ids, and account ids as part of thier state?
Slack looks like they would have all this data attached to a Message.
Am I missing your point here?
u
well they can, not sure what r u getting at?
d
FRom what I understand you are trying the query for messages but need to join them to another domain object like an Account object correct?
u
true
d
Ok, so the return object from the Join is it's own data class.
It has the message(s) as one field and the account as another.
u
what I am struggling with is the domain layer/ domain objects, how should they look; since from what I understand, there should be only one Message in the domain?
okay so youre saying this? (domain layer)
Copy code
data class Message(...)
data class Account(..)
data class MessageWithAccount(message, account) ?
d
From a query perspective yes
u
what query? this is domain layer im talking about
d
You may not be using Room as your database but they have good documentation on doing this kind of thing.
u
no they dont, this is different issue, youre talking data layer, thats no problem
domain is problematic for me; question is if there should be only one Message type in domain; and how does it related to data layer from joined queries; and how does it look like mapped from partial projections in data layer
getting joins mapped onto data classes is no problem, room and sqldelight do this automatically; after that is what Im trying to ask
unless you mix your data layer objects and domain layer objects
d
After getting the data from the CachedMessage from the DB you will then map it into the DomainMessage.
DomainMessage contains all the logic required for doing message things.
u
thats theory fine,; please how would you add account to this?
composition?
d
Account is the same
Messages hold in them the account id
The Account does not need to know about messages.
u
yes it doesnt; im asking how do you get the account in the domain layer for a given message
d
You find it by using the id attached to the message.
u
okay so youre doing 2 queries; one to get message; one to get account?
d
Network or DB queries?
u
okay use case for example is this: sync happens, somebody finds out message id = 123 is new; show notifictaion for it; so you query db = you get cachedMessage
d
Get the message first and then get the account using the account id attached to the message.
u
how will you get coresponding Channel and Account objects? You need both (or rather all three) to display a notification
okay that implies you going twice/three times to the db, am I correct?
d
All of that is meta data attached to the message object.
Copy code
Message(val accountId: String, val channelId: String, ...)
u
is this a DomainMessage?
d
Yes sir
And the DB will have this data to
u
fine; how do you get DomainAccount and DomainChannel -- is it by doing 2 queries now? (and mapping CachedAccount to DomainAccount + same for channel)
d
If the Message has no information related to where it originated then you have a problem.
u
Youre missing my point
I think youre suggesting I ran 3 queries instead of 1 join query
no?
d
You will need to make 2
one for the message
the next for the joined account and channel
Provided the API (data base or network) can return the joined data.
u
honestly that is bad
why would you run a select from messages where id =? and then select from channel where id=? and same for account
if you can run a join?
Imagine you need a list of this .. will you do N*3 queries instead of 1?
d
Look
All I am saying is that to do 1 query you need all the Ids
u
which I have, yes
d
If you can devise a way to get all the ids of the objects you require then you can get the objects in 1 shot.
u
yes message table has channel id column, and channel has account id column
sorry if I wasnt clear on that earlier
d
Take the ids and make a single query for all the objects at once.
You get a data class containing all the 3 things.
u
yes, all is good, im asking now how should the domain objects look like
d
Then you can map them all into the things you need.
u
and how do you attach stuff, should there be composition of should message have val channel, val account
d
The domain object should be things you can ask to do work.
u
yes I know; im asking what would be the signature of such method
d
If a message can not have empty text then that is encoded in the object itself.
u
if it needs domain message, domain channel, domain account
d
A mapping method?
u
no; something in the domain, for example
NotificationDisplayer.displayNotification( ...???... )
if it needs message and its channel and its account
d
give it all the data it needs to do it's job and no more.
u
omg .. should there be a 1 param in that function of some joined data class or 3 params?
d
if it only needs a partial amount of data from each object then you extract i'ts requirements into a neat little package and had it to the function.
u
not sure what do you mean; like another type of lets say DisplayNotificationInput?
d
Sure
u
what is the point of domain objects then of every function in domain will have its own input type?
d
If the notification method requires more then 3 parameters then give a data class containing those things.
Displaying a notification is not part of the domain layer
It's a UI concern
and the UI can get UI data.
u
I dont think so
d
That's your opinion
u
okay forget that, pick some other function you consider in the domain
d
But in a large complex application you will run into problems if you do not separate things.
u
huh, Im trying to, you just consinder it UI, I dont, but that is not my point
my point is -- you have a repository that returns cached message + cached channel + cached account
d
yes
u
then you have some domain interactor thing whatever you call it,
and whats the return type for such mapping
d
The interactor is not a domain object.
u
a Triple<DMessage, DChannel, DAccount>? or DMessage( .... , dChannel, dAccount) or MessageWithChannelAndAccount(dMessage, dChannel, dAccount)
this is my question
d
it is a thing that controls the interaction between domain objects and other infrastructure to accomplish a task for the user.
The third one is returned from the query.
Are you aware of GRASP?
u
yes, and I dont you understand me still
d
General Responsibility Assignment Software Patterns
u
im asking if you have a composite types in the domain layer
d
No you do not
They are separate objects
Sometimes even there own modules.
u
okay fine, how do you do returns then, tuples?
d
data classes
u
..thats a composite type
d
IT is not a composite
It is a data container
A composite is a different thing
u
a composite is composed of parts; in this instance Im thinking a container to carry around message and its channel and its account
d
Ok
u
what would be a domain function for you .. MessageSender.sendMessage ?
Copy code
fun foo() {
	val messageAndChannelAndAccount messageRepository.messageById(...id)
	val ???? = messageDomainMapper(messageAndChannelAndAccount)

	messageSender.sendMessage(needs domainMessage, domainChannel, domainAccount)
}
d
I send the message a command and it returns me an error or a list of events that describe what changed. The Message would have a
handle(command: MessageCommand): Either<MessageFailure, List<MessageEvent>>
function. That is a domain function.
That's just one example.
Another domain related thing would be validation of potential new messages.
u
okay fine, thats besides my point
im asking how do you get data to it from data layer
d
If I am doing CRUD then the Message is just a class that holds some data.
u
please see my pseudo code, the
???
is what im asking
d
The real domain work could be done by a
MessageValidator
And other classes.
oh
u
yes, thats all after it, the ??? is my source question
d
Copy code
fun foo() {
    val messageAndChannelAndAccount = messageRepository.messageById(messageId, accountId, channelId)

   val mappedClasses = messageDomainMapper(messageAndChannelAndAccount)

    messageSender.sendMessage(mappedClasses)
}
My question is why messageSender need all three classes. Is it an interactor?
Does that help?
u
I made it up, lets just say it needs because of backend api
heh .. and what is MappedClasses?
I thought there should be only DomainMessage, DomainChannel, DomainAccount in the domain layer as data types
d
That's correct
u
then what is MappedClasses...?
d
So the query gets you the composite the the cached classes ok?
u
yes its a data class of DbMessage, DbChannel, DbAccount
d
The mapper will take the composite and return you another composite containing the Domain classes.
You then pass the domain composite to the MessageSender
u
yes, that was what I written before
and you said no composites in the domain
d
The composite can be removed if it's not needed by the messageSender
the message sender can get all 3 classes put into it's function as parameters if needed.
u
okay so lets have the composite
data class DomainMessageAndDomainChannelAndDomainAccount(val message: DomainMessage, val channel: DomainChannel, val account: DomainAccount)
so this is the ONLY type that should be used when a consumer in the domain wants all three objects? or rather the plumbing should use, before destructuring it into 3 params of the consumer function?
d
If you have a function that takes all 3 of those things then you can define this as the only parameter if you like.
u
sure thats besides it, im asking how to pass the triple around
d
You already defined that
u
cool
d
You just pasted the triple
u
yes thats qnaswer to my question
d
LOL!!!!!
wow 157 messages later
u
-- what do you do about partial projections?
how do you map those into domain objects?
presuming the partial projection has less fields queried than target domain objet
d
You cant really have a partial of a domain class.
That is just some data.
Let me give you an example
A Message has attachments and some text ok?
but you only have the attachments.
Then you make a new message and set its attachments to the ones you possesses and it's text to ""
Now you have a whole message class
u
well thats just easy because of its a string and emptyList()
what if its some type
okay so that means you always do SELECT *?
d
if you can not create a default empty implementation of the type you can just make that type nullable
Or an Option<T>/Maybe<T>
yes
SELECT *
get the data you can to hydrate a domain Message class.
u
okay and if that becomes a perf problem?
default values?
d
what about default values?
u
lets say your domain message is 50 fields because of some use case
but sometimes you only need id and text
Copy code
val messageIdAndText = messRepo...
val domainMessage = DomainMessage(messageIdAndText.id, messageIdAndText.text, ....now what)
d
SELECT * FROM message_id WHERE id = "blarg"
join that with the text
Something like that
My SQL is rusty
u
so you dont care about perf difference of querying 2 columns vs 50 via * ?
d
Don't get a whole message object if you only need 2 fields.
Get the 2 fields
u
fine, but how does that get mapped into domain .. rest of vals on DomainMEssage with null or default values?
or should there be some hierarchy od domain message variations
d
Both work
It depends on your use case
u
well that was my original question, if there should be only 1 Message type in domain or can there be more
and if they should be related somehow via interfaces
d
MMS Message is not SMS Message but both could have id and text.
u
that sure, im meaning more like
Copy code
interface SimpleMessage {
   id, text
}

interface FullMessage : SimpleMessage {
    channel, account, whatever
}
d
If you have a function that takes a Message that has an id and text but the function does not care about whether it is MMS or SMS then you can have an IMessage interface.
Yes
That's the ticket.
u
thats different messages, im thinking more of like scoping the same data
d
An interface does that
u
didnt know if that is valid in CLEAN
d
It is valid in SOLID
lol
u
alright acronyms
one more thing I want to ask
d
The L in SOLID is what we are talking about here.
u
sure, then again you can pass data layer all the way to ui
d
Liskov's Substitution Principal fits the bill here.
Hmm
I would not go that far.
u
I know how polymorphism works lol, im talking about architecture and layering
what I wanted to ask is .. this all presumes mapping like this api message -> db message -> domain message
d
A common interface is fine
The domain defines the interface though.
u
that is basically for displaying the chat messages
now I need to search messges -- search is a backend feature, I dont have all messages locally
d
ok
u
so the mapping should go api message -> domain message directly?
I presume yet, but, that duplicates the mappers
d
If you want to bypass the DB then yes
u
and that can be error prone duplication
thats just the way it is and should write unit tests?
d
RemoteMessage(): IMessage
maps to
DomainMessage(): IMessage
No
u
huh why? isnt RemoteMEssage(=ApiMessage) part of data layer?
d
The reason we do the mapping is to make sure the data from the outside world is clean for our domain.
u
yes, why do you have the interface there then? IMessage is domain layer no?
d
Can we do a call?
Maybe if I spoke to you in person it would help
u
sorry cant call right now
d
ok
No worries
u
I mean in general im good
but what worries me is
is mapping from api msg to domain msg being bugly different from api -> db -> domain msg
unless, you create a dummy db message just for pourpose of mapping
d
ApiMessage ->UIMessage
user modifies the message ok?
UIMessage -> DomainMessage
u
not sure what you mean, Im talking about search, where search is remote
d
Ask the now DomainMessage to handle the users command
I understand
u
and my issue is I worry about ApiMessage -> DomainMessage mapper being different from ApiMessage -> DbMessage -> DomainMessage
d
If you just want to grab the message from remote and show it on the screen then do api to ui mapping
u
or even UiMessage, doesnt matter, same problem
of skipping mapping layers
and how to keep the mappers consistent
and the question is if I should create dummy layer objects just for purposes of mapping, so I have a single pipeline
d
Say all messages must have 3 things
accountId, channelId, and id
then domain makes an interface
Copy code
interface IMessage {
 val id: String
 val accountId: String
 val channelId: String
}
u
ok
d
everything that is considered to be a message implements that interface.
When the message hits the domain from anywhere it will be ok because everything conforms to that interface.
Everything has a dependency on the domain
u
that is probably fine, what im getting at is forgetting some step as in to preprocess message text for mentions for example
d
and the domain determines the rules around the data
you could build that into the constructor of the classes?
u
or maybe mapping enums .. lets say ApiMessage has some pseudo type on it of "foo", "bar", etc; DbMessage has the same enum just mapped to 0 1 itnegers; and DomainMessage needs real enum type of Type.FOO, Type.BAR
d
Copy code
init {
 doPreprocess
}
u
so now you have two mappers for this "foo" -> Type.FOO; and "foo" -> 0 -> Type.FOO
and this is what I worry about getting out of sync
d
You only have so many solutions
u
well yes, im asking about your opinion .. you can craete dummy dbMessage for the first case, then its always correct but you create garbage
or 2nd options: unit tests
d
You either embed the logic in the constructors, intercept the API DTO and do the pre-processing there, or shove it in the mappers.
Unit test is not an option
it is a requirement
lol
u
well its either compiler or tests as enforcement; id much prefere compiler
d
You could use sealed classes and when expressions to ensure new types of messages added to each layer are handled by the mappers.
I did that in an SDK I built
When I added a new DTO it would have to inherit from the sealed class. Then my code would not run with out updating the mapper.
sealed class Message
I have to go
But I am sure you will make the right choices.
u
okay thats a lot for your help
d

https://youtu.be/sOaS83Ir8Ck

That talk might help you
I build my stuff this way and it has helped me a lot.
u
what is it about, about my mappers sync issue?
d
Not directly, it is about where everything in your project should go and why.
Indirectly it might help you make the choices you need to
u
k k thanks