Use cases show us the intent of the software. They describe the behavior of a system and its interactions with users. They are likely the first thing to be listed when we think about a new application.
Despite the above, it seems to me that the role of the use case is not entirely clear within the Android community.
This article is a summary of how I understand the role of a use case. It is based on principles of Clean Architecture and my own experiences, filled with attempts and failures while implementing use case objects on a medium-size Android project.
You might not agree with everything; please share your thoughts in the comments or reach out to me through Twitter. The truth is born in dispute.
I hope this blog post — along with the subsequent discussion — helps us find new ideas and avoid some pitfalls in the future.
The Theory
A use case is a description of a software’s usage. It specifies a scenario in which a system receives an external request (such as user input) and responds to it by performing one or more actions. It can be written or visualized with the help of a use case modeling tool, e.g., Use Case Diagram.
Use cases provide a way to cut the system vertically through the horizontal layers of the system. Each use case utilizes some UI, domain logic, data logic or whatever the system is made from.
A use case object, also known as Interactor
, encapsulates and implements use cases of the system. It specifies input parameters, return type and the processing rules for achieving the goal of the use case.
The rules are application-specific and only make sense in the scope of the application. They specify operations above domain “models” that encapsulate so-called critical business data and rules that might be, on the other hand, valid not just in the context of the application.
Example of use case vs. domain “model”
The use case object is not aware of the existence of UI or a database. It describes the input contract and output contract, and it is the caller’s responsibility to fulfill them. The use case object does not know who calls it and whether it delivers results to an Android or iOS application, or even the web. If you had Android and iOS apps with identical requirements, you should be able to share use case objects between them using cross-platform tools.
The use case object does not describe what the UI looks like. It describes the application-specific rules that dictate the interaction between users and the system. Therefore, a use case object should be called by a component handling inputs to the app, e.g., UI or Controller, not Repository.
The Implementation
A use case object implements one business flow. Therefore, it should expose functions for executing the single flow.
It might seem logical that the use case object should contain just one function, which is what I’ve seen the most. However, I do not think it is strictly necessary. From my perspective, the number of functions should depend on the level of abstraction you want to achieve, as long as the use case object encapsulates one functionality.
For example:
class SignInUseCase { suspend fun signInWithEmail(email: String, password: String): User { ... } suspend fun signInWithGoogle(serverAuthCode: String?): User { ... } }
vs.
class SignInWithEmailUseCase { suspend fun signIn(email: String, password: String): User { ... } } class SignInWithGoogleUseCase { suspend fun signIn(serverAuthCode: String?): User { ... } }
Either you have two functions in one class or two classes with one function. Both describe the same interaction of the same actor (the user signing in). In my opinion, there are situations when there is no major advantage to using one option over another, or to combining both. The final decision regarding which way to go should most often be based on the size and structure of the project, the complexity of the implemented logic or just preferences of the coding conventions a team has established.
A use case object does not have to contain just one function, as long as it encapsulates one functionality.
The return type of functions can be any arbitrary type, including those that provide a stream of data, like Flow
or Observable
, if it is required for successful completion of the use case.
But! Never wrap two different interactions, e.g., “Sign in” and “Sign out” in one use case object. That would no longer be a use case object as it is defined. Whenever you are not sure about whether to split, my advice is to draw a simple UML Use Case Diagram as a reference for the given actor. When you end up drawing two separate ellipses (without a relationship), you likely need two use case objects.
Job Offers
Composition
A use case object encapsulates the logic of a use case. It can be executed with the support of adjacent layers like UI and data to achieve the goal of the use case.
Thus, in my opinion, two use case objects should be composed only if the first one requires the support of the second to achieve the goal of its use case.
For example, RegisterUserUseCase
and VerifyEmailUseCase
. The result of registration is dependent on the result of email verification. Email verification supports the “Register user use case” to achieve its goal. No doubt here.
A question might arise when you design a use case object that describes browsing or “viewing” something. A simple version of such a use case object just fetches data.
Let’s consider “View profile” and “Change password” use cases.
A potential ViewProfileUseCase
returns data about the user fetched from a data source so that the goal of the user’s interaction is achieved — view profile.
class ViewProfileUseCase( private val dataSource: DataSource ) { suspend fun getUserProfile(): User { return dataSource.getCurrentUser() } }
“Change password” use case allows the user to change the password if the provided current password matches the actual one.
class ChangePasswordUseCase { suspend fun changePassword(current: String, new: String): User { ... } }
To complete the flow, it is necessary to fetch the current user for password comparison.
Should ViewProfileUseCase
be injected in ChangePasswordUseCase
to get users’ data?
Source: https://cz.pinterest.com/pin/704602304185711164/
In my opinion, it should not.
ChangePasswordUseCase
does not depend on ViewProfileUseCase
in achieving its goal in any way. It is not required to “view profile” in order to “change password”. It should be the responsibility of a data-access component (in a possible data layer) to support the use case object providing data.
Considering a UML diagram, you would hardly think about any relationship between those two use cases. The same should be expressed in code.
The other way around: If you were first required to implement “Change password” would you think about creating a use case object to fetch data? And if, later on, you were asked to implement “View Profile”, would you come back to ChangePasswordUseCase
to replace the data-access call with ViewProfileUseCase
call? I do not think so.
One might temp to compose use case objects to reuse some data-access-related logic, like caching or error handling. But use case composition is not what you need! Most likely, you need to add another layer.
How do you understand the role of the use case?
Resources:
- Martin, R.C. (2018). Clean Architecture. Pearson Education, Inc.