Before we dive into the UseCase of Fluent and Fun Clean Architecture, let’s first understand how to implement it in Clean Architecture.
For better understanding, Let’s consider a simple example of a use case in an e-commerce application:
Use Case: Place Order
Action: Customer
Description: This use case represent the process of a customer placing an order for a product in the e-commerce application.
Requirements:
- The Customer should be logged in.
- The cart is not empty to be able to place an order.
- The amount of money in the wallet of the customer should not be less than the amount of the ordered item.
- If all three are met, update the product stock.
- Clear the cart in the app once placing an order is finished.
To implement it in Clean Architecture, you would typically follow a code structure similar to the one shown below.
class PlaceOrderUseCase(
private val userRepository: UserRepository,
private val productRepository: ProductRepository
) {
operator fun invoke(order: Order) {
if (userRepository.isLoggedIn()) {
val cart = userRepository.getCart()
if (cart.isNotEmpty()) {
if (userRepository.hasEnoughFunds(order.getTotalPrice())) {
productRepository.updateProductStock(order)
userRepository.clearCart()
} else {
throw InsufficientFundsException(
"Not enough funds in the wallet."
)
}
} else {
throw EmptyCartException("The cart is empty.")
}
} else {
throw NotLoggedInException("User is not logged in.")
}
}
}
Now, let’s break it down step by step to create a Use Case:
- Create a class with a name that consists of verb in present tense + noun/what (optional) + UseCase.
- Define the necessary parameters, which can be either Repositories or other Use Cases, depending on your application’s requirements.
- In Kotlin, you can make Use Case class instances callable as functions by defining the
invoke()
function with theoperator
modifier.
For further insights, you may refer to the official Android developer documentation, where you can explore how they define their domain layer and Use Cases, which shares similarities with our approach above.
In the context of Fluent and Fun Clean Architecture, the objective is to achieve a similar implementation as described below.
OrderUseCase.ktfun OrderRepository.placeOrder(order: Order) {
whenUserLoggedIn {
whenCartNotEmpty {
withSufficientFunds {
updateProductStock(order)
clearCart()
}
}
}
}
The primary objective is to provide a clear and concise summary of the necessary steps when placing an order. In the following code snippet, you will notice that it reads like natural language, making the use case instantly understandable. Before placing an order, several checks must be completed.
Here’s the breakdown of the implementation:
- Instead of using a class, we’ve created a file named “OrderUseCase.”
- The file name follows the convention of noun/what (optional) + UseCase.
- There is no class involved; instead, we utilized a top-level extension function or a regular top-level function.
- The function itself represents the use case.
- The function’s name serves as the primary use case to be called from the presentation layer.
- Readability and Expressiveness: By using a chain of function calls with meaningful names (
whenUserLoggedIn
,whenCartNotEmpty
,withSufficientFunds
), the code becomes more readable and expressive. Each step of the process is clearly outlined, making it easier to understand the flow of the use case. - Simplified Control Flow: The chain of function calls follows a natural flow of conditions that need to be met to place an order. This simplifies the control flow and makes the logic more straightforward.
- Modularity: The code separates concerns into different functions (
whenUserLoggedIn
,whenCartNotEmpty
,withSufficientFunds
), making each function handle a specific check or condition. This promotes modularity and makes it easier to change or add new checks without affecting the entire use case. - Conciseness: The code avoids the need for a separate use case class with boilerplate code. It condenses the logic into a concise and focused extension function.
- Fluent Interface: The use of the
when
keyword in the function names (whenUserLoggedIn
,whenCartNotEmpty
) gives the code a fluent interface style, enhancing readability and making it more similar to natural language. - Clarity of Intent: The breakdown of the use case logic into separate functions allows the developer to clearly express their intent at each step of the process. It becomes evident what each condition represents in the context of placing an order.
Overall, we prefer this approach because it offers a more concise, expressive, and modular way of defining use case logic. It reduces boilerplate code and helps developers focus on the specific steps and conditions required to place an order. Additionally, it can be seen as an example of how functional programming concepts, such as chaining and fluent interfaces, can be applied to design more readable and expressive code.
This is a valid question. Before providing an answer, let’s begin by defining what a class is in programming.
- Classes are used to define blueprints for creating objects. You can create multiple instances (objects) of a class with different state and behavior.
- Classes can have properties, member functions, and constructors, allowing you to create instances with varying initializations and states.
- If you need to create multiple instances of a concept (e.g., multiple users, products, etc.), you would typically use a class.
What is state and behavior in class?
- State represents the current data or properties of each object. It’s represented by instance variables (fields) within the class, and each object created from the class has its own set of these variables. For example, in the Car class, state attributes could include color, make, model, year, and fuel level, with each instance having its specific values.
class Car(private val color: String,
private val make: String,
private val model: String,
private val year: Int) {
private var fuelLevel: Double = 0.0
}
- Behavior in a class defines the actions or operations that objects can perform. It’s represented by methods within the class. These methods determine how objects interact with others and the outside world. For example, in the Car class, we might have methods like start(), accelerate(), brake(), and refuel() to represent the various behaviors a car object can exhibit based on its state.
class Car {
private var fuelLevel: Double = 0.0// Behavior (methods)
fun start() {
println("Car has started.")
}
fun accelerate() {
println("Car is accelerating.")
}
fun brake() {
println("Car is braking.")
}
fun refuel(amount: Double) {
fuelLevel += amount
println("Car has been refueled with $amount gallons.")
}
}
Answering the question:
As we can see, classes are indeed a powerful concept in programming. However, for certain use cases in Kotlin, we might not fully utilize the capabilities of a class if we use it only to invoke a single behavior or function.
In such scenarios, opting for a function instead of a class provides a more straightforward and concise approach. A function allows us to directly define the logic for the use case without introducing unnecessary class-related syntax and structure. It also aligns with functional programming principles, encouraging us to design use cases as pure functions, which are predictable, testable, and less prone to side effects.
To illustrate this point, let’s consider a simple analogy. Think of a Car as a class and walk as a function. If you only need to go to a place that is two to three minutes away, using a powerful Car with music, GPS, and air-conditioning might be overkill. In this case, you have the option to walk, which is a simpler and more direct approach.
Similarly, imagine a Phone as a class and talk as a function. If you want to say something to someone in the same room, using a powerful phone with various functionalities might not be necessary. You can opt to talk to that person directly, which is a more straightforward way to achieve your goal.
The key takeaway here is that there are different ways to accomplish tasks, and sometimes, a functional approach can be more suitable and efficient. By considering a functional approach first, we can focus on simplicity, readability, and expressive code. It is essential to choose the right tool for the job and leverage the appropriate concepts in software development. Embracing functional programming concepts in Kotlin can lead to cleaner and more maintainable code, ultimately enhancing the development experience.