[MUSIC PLAYING]
DON TURNER: Hello, and welcome to the fourth episode
of the MAD Skills Architecture Series.
In this episode, I'll talk about the domain layer,
an optional layer, which sits between the data and UI layers.
Now, I know what you're thinking,
another layer in my app architecture?
Why would I need another layer?
Well, the domain layer can actually
simplify your architecture, making it easier to understand,
as well as more scalable and simpler to test.
So let's dive straight in.
The domain layer holds business logic,
a set of rules and actions that make your app valuable.
Business logic is different to UI logic.
UI logic defines how to display things on screen,
whereas business logic defines what to do with events and data
changes.
Examples of business logic include
getting the latest news articles in a news app
or showing an error message if there's no internet connection.
Now, in smaller apps, this logic is often held in view models
or in the data layer, but as the app grows,
it may make sense to consolidate all that logic
into a new layer, the domain layer.
It's important to note that the domain layer is not
responsible for how data is displayed.
That's the job of the UI layer.
And it's not responsible for storing or retrieving data.
That's the job of the data layer.
So in summary, the domain layer only holds business logic.
The domain layer is made up of classes known
as interactors or use cases.
And a use case represents a single task
which the user can do or which the app
performs on the user's behalf.
So in this episode, we'll use the following convention
for naming use case classes--
a verb representing what you're doing,
a noun describing the object that you're doing it to,
and the use case suffix.
Now, using a suffix like use case on every class
might seem like overkill, but it can
make code much more readable, especially in large code bases.
That said, this naming convention
is just one way of naming your use cases.
Feel free to use whatever works for you.
So use cases should be simple, lightweight, and immutable.
If you need to cache data, this logic
might be better placed in the data layer.
Use cases can be dependent on lower layers,
such as repositories in the data layer, and on other use cases.
But they shouldn't be dependent on higher layers, such as view
models.
They should expose data and operations to the UI layer
the same way that repositories do, using suspend functions
or flows in Kotlin, or callbacks if you're not using Kotlin.
And if you're not using Kotlin, deeply sorry for you.
Let's take a look at some examples of use cases.
In a news app, we might want to display articles together
with some information about the article's author.
To do this, we can create a use case which gets the latest news
articles from a news repository and combines it with data
from an author's repository.
Let's say we also want to just display the article publication
date in the user's local format and time zone.
This could be implemented in a format date use case
and used by our first use case.
The important thing to remember here
is that use cases contain reusable logic,
so they can also be used by other use cases.
And it's perfectly normal to have multiple levels of use
cases in the domain layer.
Since use cases do just one thing,
you can take advantage of Kotlin's invoke operator
to make a use case instance callable as
if it were a normal function.
When you pass your use case as a dependency,
for example, when constructing a view model,
you can call your use case like a normal function.
This makes calling use cases concise
because it avoids the need for an extra redundant method.
One of the advantages of using the use case suffix
can be seen here.
It makes it obvious that you're calling a domain layer class
rather than a normal function.
So use cases don't have their own lifecycle.
Instead, they're scoped to the class that uses them.
Because use cases don't contain mutable data,
you can safely create a new instance of a use case class
every time you pass it as a dependency.
Now, let's talk about threading.
Use cases should be main-safe, as in, it
should be safe to call a use case from the main or UI
thread.
If a use case is performing a long-running,
blocking operation, then move it to a background thread,
but consider whether that blocking operation
would be better handled in the data layer
so that the results can be cached.
OK, let's take a deeper look at how
to perform common tasks in the domain layer.
Firstly, encapsulating reusable business logic.
If you have logic which is used by multiple view models,
then place this logic inside a use case.
For example, in our news app, we have some common logic
which is used to format dates according
to the user's formatting and timezone preferences.
Traditionally, this kind of logic
can often be found inside static methods of util classes.
These methods are usually hard to find
and can become a space for miscellaneous functions
without a clearly-defined purpose.
Moving this type of logic to a use case
clarifies its role in the app architecture.
Additionally, use cases can share common functionalities,
such as threading and error handling
from base classes, which can benefit larger teams at scale.
The second common task which use cases can perform
is combining data from multiple repositories.
Let's expand on our news app example from before.
We have two repositories, NewsRepository
and AuthorsRepository, that handle news and author data
operations respectively.
We want to display more information
about the author next to each news article,
but the NewsRepository exposes only the ID of the author.
Information can be obtained from the AuthorsRepository.
Now, we could place this logic directly in a view model,
but combining repositories can involve complex logic.
Having this logic inside view models
can make them unnecessarily large, complicated,
and potentially more difficult to test.
A better solution is to create a use case to combine the data.
This avoids bloating our view model with logic
and creates an easily testable unit
which can be reused wherever the functionality is needed.
Let's take a look at the implementation.
The use case takes the repositories as dependencies,
as well as the dispatcher to be used for background work.
Here, we're using the invoke operator
to make the use case callable, but you could also
use a normal function.
Whatever works for you.
The function returns a model which
contains the combined data.
And inside the function, we process
each of the articles received from the news repository
and combine them with the author information.
This is done on background thread
because although the data layer is main-safe,
we don't know how many articles we'll be processing,
and therefore, should not block the calling thread.
One other consideration when implementing the domain layer
is whether you should still allow direct access to the data
layer from the UI layer or force everything through the domain
layer.
Now, some advantages of making this restriction
are that it stops your UI from bypassing domain layer logic,
for example, if you're performing
analytics logging on each access request to the data layer.
It can also make unit testing view models easier
since they only ever depend on use cases
rather than on repositories as well.
However, the potentially very significant disadvantage
is that it forces you to add use cases,
even when they're just simple function
calls to the data layer, which can add complexity
for little benefit.
In most cases, it makes sense to add use cases only when
needed, allowing the UI layer to access the data
layer as required.
Although, as always, it comes down to your individual code
base and whether you like strict rules or a more flexible
approach.
So in summary, the domain layer is
used for encapsulating business logic
and can be used to reduce the complexity of the UI layer.
It can also be used to avoid duplication
by extracting logic used by multiple view models
into a single use case.
And lastly, it can improve testability.
Logic is much easier to test when
it's contained in a small classes which
only do one thing.
So that's it for the domain layer.
Now you know what it is and how to use it,
you can decide whether it's right for your code base.
This talk is just a summary of the mad architecture guidance,
so for more information, check out the full guide here.
That's all for now.
If you have found this video useful, please give it a
thumbs up.
Also, to be notified of more videos like this,
please do subscribe to this channel.
Thanks very much.
[MUSIC PLAYING]