UseCase Usages and Best Practices

UseCase Usages and Best Practices

Explore UseCase Usages and Best Practices using Kotlin

ยท

3 min read

First, Is UseCase a Design Pattern?:

The UseCase pattern is not a standalone design pattern in the traditional sense like Observer or Strategy patterns. Instead, it's a concept that is commonly used within the context of software architecture, particularly in architectures like Clean Architecture and Domain-Driven Design (DDD).

In essence, a UseCase represents a single, specific task or action that the system needs to perform. It encapsulates the business logic necessary to fulfill that task. UseCases are often used to separate the business logic from the rest of the application, promoting modularity, testability, and maintainability.

What is UseCase?:

While UseCase itself may not be considered a standalone design pattern, it embodies principles of separation of concerns and single responsibility, which are core tenets of good software design. It's more of a structural and organizational concept rather than a formal design pattern.

In this article we will delves into the principles and practices of clean architecture, focusing particularly on the role and characteristics of UseCases.

Usages Examples and Explanation:

  1. Responsibility of UseCase:

    • UseCase encapsulates business logic for a single reusable task.

    • It should focus on what the product team needs to achieve.

    • Example: A payment UseCase performing tasks like starting a transaction, sending payment, and finalizing the transaction.

    class SendPayment(private val repo: PaymentRepo) {
        suspend operator fun invoke(amount: Double, checkId: String): Boolean {
            val transactionId = repo.startTransaction(params.checkId)
            repo.sendPayment(amount = params.amount, checkId = params.checkId, transactionId = transactionId)
            return repo.finalizeTransaction(transactionId)
        }
    }
  1. Key Points on UseCase:

    • Each UseCase should perform only one task, ensuring re-usability and clarity.
    // Example of a UseCase performing a single task
    class SaveImageUseCase @Inject constructor(/* dependencies */) {
        operator fun invoke(file: File): Single<Boolean> { /* logic for saving image */ }
    }
  • Thread safety: Heavy operations should be handled on a separate thread.
    // Example of ensuring thread safety in a UseCase
    class AUseCase @Inject constructor(
       @IoDispatcher private val dispatcher: CoroutineDispatcher,
    ) {
        suspend operator fun invoke(): List<String> = withContext(dispatcher) {
            val list = mutableListOf<String>()
            repeat(1000) { list.add("Something $it") }
            list.sorted()
        }
    }
  • Red flags include using non-domain classes, having more than one public function, containing non-general business rules, and handling mutable data.
  1. Common Questions:

    • Abstraction with UseCases is optional unless multiple implementations are needed.

    •       // Example of an abstract UseCase interface and its implementation
            interface GetSomethingUseCase {
                suspend operator fun invoke(): List<String>
            }
      
            class GetSomethingUseCaseImpl(private val repository: ChannelsRepository) : GetSomethingUseCase {
                override suspend operator fun invoke(): List<String> = repository.getSomething()
            }
      
    • UseCases can be redundant if they only wrap repository functions, but they can offer benefits in terms of code clarity, consistency, and future-proofing.

    // Example of a UseCase wrapping a repository function
    class GetSomethingUseCase @Inject constructor(private val repository: ChannelsRepository) {
        suspend operator fun invoke(): List<String> = repository.getSomething()
    }
  • It's acceptable and recommended to use UseCases within other UseCases for better modularity and reusability.
    // Example of using one UseCase within another
    class CompositeUseCase @Inject constructor(
        private val getSomethingUseCase: GetSomethingUseCase,
        private val saveSomethingUseCase: SaveSomethingUseCase
    ) {
        suspend operator fun invoke() {
            val data = getSomethingUseCase()
            saveSomethingUseCase(data)
        }
    }

Conclusion:

Overall, the article provides insights into how to effectively structure and implement UseCases within clean architecture, highlighting the importance of adherence to single responsibility and clarity, while also addressing common concerns and providing practical examples.


That's it for today. Happy coding...

ย