0
Java

Clean Code Principles in the Use Case Layer

In the world of software development, maintaining a clean and manageable codebase is crucial for long-term success. Robert Martin's Clean Code offers invaluable principles and patterns to achieve this. Today, I want to introduce the first part of a multi-part series to discuss how we apply these principles to our use case layer, ensuring it remains isolated and adheres to our coding standards. This series will comprehensively examine how we layout our code using clean code patterns to maintain a robust and scalable codebase.

In the world of software development, maintaining a clean and manageable codebase is crucial for long-term success. Robert Martin's Clean Code offers invaluable principles and patterns to achieve this. Today, I want to discuss how we apply these principles to our use case layer, ensuring it remains isolated and adheres to our coding standards.

Use Case Layer: The Heart of Business Logic

Our use case layer encapsulates the core business logic. It operates independently of external layers, ensuring a clear separation of concerns. This isolation means our business domain objects do not cross any boundaries, maintaining the integrity and purity of our domain logic.

Interface Definition

For each use case, we define an interface. This interface aligns with a user story, providing a clear contract for what the use case achieves.

public interface GetUserAccount {
    Stream<UserAccountDto> execute();
}

The execute command method is a convention we follow to maintain consistency across our use cases.

Implementation Example

Let's dive into an implementation example to see these principles in action.

public class GetUserAccountImpl implements GetUserAccount {
    private final Function<UserAccountGatewayDto, UserAccount> userAccountGatewayDtoUserAccountMapper;
    private final Function<UserAccount, UserAccountDto> userAccountToUserAccountDtoMapper;
    private final GetUserAccountGateway getUserAccountGateway;

    public GetUserAccountImpl(
        GetUserAccountGateway getUserAccountGateway,
        Function<UserAccountGatewayDto, UserAccount> userAccountGatewayDtoUserAccountMapper,
        Function<UserAccount, UserAccountDto> userAccountToUserAccountDtoMapper) {
        this.getUserAccountGateway = getUserAccountGateway;
        this.userAccountGatewayDtoUserAccountMapper = userAccountGatewayDtoUserAccountMapper;
        this.userAccountToUserAccountDtoMapper = userAccountToUserAccountDtoMapper;
    }

    @Override
    public Stream<UserAccountDto> execute() {
        try {
            return getUserAccountGateway
                .execute()
                .map(userAccountGatewayDtoUserAccountMapper)
                .distinct()
                .map(userAccountToUserAccountDtoMapper);
        } catch (GatewayException e) {
            throw new FailedToGetUserAccountException(e);
        }
    }
}

Key Points

  1. Constructor Injection:
    • Dependencies are injected via the constructor, promoting immutability and making the class easier to test.
    • No annotations are used within the class; configuration is managed externally, typically in a Spring configuration class.
  2. Gateway Pattern:
    • The use case calls a gateway interface, isolating the use case from the implementation details of the gateway.
    • This promotes a clean separation between the use case and data access layers.
  3. Mapping and Business Logic:
    • Domain objects are mapped to DTOs to cross boundaries, ensuring the use case only deals with domain objects.
    • Business logic is applied to the domain objects before mapping the results back to DTOs for the controller layer.
  4. Builder Pattern:
    • Mappers use the builder pattern to create objects, adhering to our principle of not directly instantiating objects (new keyword) within our code.
    • This pattern enhances readability and maintainability, ensuring that object creation is consistent and controlled.

Configuration and Dependency Injection

The Spring configuration class handles the wiring of dependencies, further promoting the separation of concerns.

@Bean
public Supplier<GetUserAccount> getUserAccountSupplier() {
    return GetUserAccountImpl::new;
}

Dependencies are supplied via a factory method, ensuring that the use case implementations are easily configurable and testable.

Conclusion

By adhering to these principles, we ensure our use case layer remains clean, isolated, and maintainable. The separation of concerns, reliance on constructor injection, and use of design patterns like the builder pattern all contribute to a robust and scalable codebase. In future posts, we'll delve deeper into specific aspects such as the configuration of Spring beans and the implementation of the gateway interfaces.