Làm sạch mã nguồn Java: Hướng dẫn thực tế về Vavr

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Thực trạng lộn xộn của xử lý lỗi theo phong cách mệnh lệnh

Java đã phát triển đáng kể từ khi phiên bản 8 giới thiệu Lambdas và Streams. Tuy nhiên, hầu hết các mã nguồn doanh nghiệp vẫn bị kẹt trong thói quen lập trình mệnh lệnh (imperative). Nếu bạn đang xây dựng các dịch vụ với Spring Boot, có lẽ bạn đã thấy mô hình này: các phương thức ngập trong các kiểm tra if (obj != null) và các khối try-catch sâu ba tầng. Điều này khiến logic nghiệp vụ thực tế gần như không thể tìm thấy.

Checked exception lại thêm một tầng gây ức chế khác. Chúng buộc bạn phải xử lý lỗi ngay lập tức hoặc làm bẩn chữ ký phương thức (method signature) bằng cách đẩy chúng lên ngăn xếp (stack). Điều này thường dẫn đến “Kim tự tháp diệt vong” (Pyramid of Doom), nơi logic cốt lõi của bạn bị chôn vùi dưới năm tầng thụt lề. Chuyển đổi từ mã “chỉ hoạt động được” sang mã thực sự bền bỉ (resilient) là một cột mốc quan trọng trong sự nghiệp của bất kỳ lập trình viên nào.

Vavr: Công cụ lập trình hàm bạn đang thiếu

Vavr (trước đây là Javaslang) thu hẹp khoảng cách giữa gốc rễ mệnh lệnh của Java và lập trình hàm. Nó không cố gắng thay thế thư viện tiêu chuẩn. Thay vào đó, nó cung cấp các lựa chọn thay thế mạnh mẽ hơn cho những thứ mà Java chưa thực hiện hoàn hảo. Trong khi Java 8 mang đến cho chúng ta Optional, Vavr cung cấp Option, Try, Either và các cấu trúc dữ liệu bất biến (persistent data structures) hoạt động một cách nhất quán.

Để bắt đầu sử dụng, hãy thêm phụ thuộc này vào tệp pom.xml của bạn:

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.10.4</version>
</dependency>

Ba công cụ giúp logic sạch hơn

Hãy cùng xem xét ba thành phần hiệu quả nhất mà Vavr cung cấp cho việc lập trình hàng ngày.

1. Xử lý Null tốt hơn với Option

java.util.Optional tiêu chuẩn là một bước tiến, nhưng nó có hạn chế. Ví dụ, nó không thể tuần tự hóa (not serializable), điều này có thể làm hỏng các tầng Redis caching hoặc RMI của bạn. Option của Vavr là một vùng chứa mạnh mẽ hơn, có thể là Some (giá trị tồn tại) hoặc None (trống).

import io.vavr.control.Option;

public class UserService {
    public Option<String> getUsername(Long id) {
        String name = repository.findNameById(id); // có thể là null
        return Option.of(name);
    }
}

// Cách sử dụng
getUsername(1L)
    .map(String::toUpperCase)
    .getOrElse("KHÁCH");

Việc sử dụng Option buộc bạn phải xử lý các giá trị thiếu ngay tại thời điểm biên dịch. Sự thay đổi đơn giản này giúp loại bỏ NullPointerExceptions trước khi mã của bạn thực thi.

2. Quản lý Exception với Try

Vùng chứa Try là một công cụ thay đổi cuộc chơi khi đối mặt với các checked exception. Thay vì logic của bạn dừng lại đột ngột với một lệnh throw, Try ghi lại kết quả. Nó trả về Success hoặc Failure, cho phép bạn tiếp tục luồng xử lý.

Đây là cách bạn xử lý một tác vụ phân tích cú pháp JSON thông thường:

import io.vavr.control.Try;
import com.fasterxml.jackson.databind.ObjectMapper;

public Try<User> parseUser(String json) {
    ObjectMapper mapper = new ObjectMapper();
    return Try.of(() -> mapper.readValue(json, User.class));
}

// Xử lý kết quả
parseUser(jsonString)
    .onSuccess(user -> log.info("Đã phân tích người dùng: " + user.getName()))
    .onFailure(ex -> log.error("Lỗi phân tích cú pháp: " + ex.getMessage()))
    .getOrElse(new User("Mặc định"));

Logic của bạn giờ đây giống như một câu chuyện mạch lạc từ trên xuống dưới. Không còn các khối try-catch làm gián đoạn luồng hình ảnh của các phương thức.

3. Thể hiện các quy tắc nghiệp vụ với Either

Các lỗi kỹ thuật như IOException nên thuộc về Try. Nhưng còn các quy tắc nghiệp vụ thì sao? Nếu một người dùng cố gắng rút 500$ từ một tài khoản chỉ có 200$, đó không phải là một “ngoại lệ” (exception) — đó là một kết quả nghiệp vụ hợp lệ và có thể dự đoán được. Either là sự lựa chọn hoàn hảo cho việc này. Theo quy ước, phía “Left” giữ lỗi và phía “Right” giữ kết quả thành công.

import io.vavr.control.Either;

public Either<String, Double> withdraw(Double amount) {
    if (amount > balance) {
        return Either.left("Số dư không đủ");
    }
    return Either.right(balance - amount);
}

Ví dụ thực tế: Đăng ký người dùng an toàn

Hãy kết hợp các công cụ này để xây dựng một dịch vụ đăng ký. Dịch vụ này phải kiểm tra đầu vào, lưu vào cơ sở dữ liệu và gửi email.

import io.vavr.control.Either;
import io.vavr.control.Try;

public class RegistrationService {

    public void processRegistration(String username, String email) {
        validateInput(username, email)
            .flatMap(this::saveToDatabase)
            .map(this::sendEmail)
            .peek(user -> System.out.println("Đăng ký hoàn tất cho: " + user.getName()))
            .getOrElseGet(error -> {
                System.err.println("Đăng ký thất bại: " + error);
                return null;
            });
    }

    private Either<String, User> validateInput(String name, String email) {
        if (name == null || name.isBlank()) return Either.left("Tên không hợp lệ");
        if (!email.contains("@")) return Either.left("Email không hợp lệ");
        return Either.right(new User(name, email));
    }

    private Either<String, User> saveToDatabase(User user) {
        return Try.of(() -> repository.save(user))
                  .toEither()
                  .mapLeft(throwable -> "Lỗi cơ sở dữ liệu: " + throwable.getMessage());
    }

    private User sendEmail(User user) {
        emailService.sendWelcome(user.getEmail());
        return user;
    }
}

Hãy chú ý cách phương thức processRegistration vận hành. Nếu xác thực thất bại, các thao tác flatMapmap sẽ tự động bị bỏ qua. Lỗi sẽ rơi thẳng xuống trình xử lý getOrElseGet. Mô hình này, được gọi là “Railway Oriented Programming” (Lập trình hướng đường ray), giúp tách biệt “luồng xử lý thành công” (happy path) khỏi việc xử lý lỗi.

Các bước tiếp theo

Chuyển sang Vavr đòi hỏi một sự thay đổi trong tư duy. Bạn ngừng coi các thất bại là những gián đoạn bất ngờ mà bắt đầu coi chúng như các phép biến đổi dữ liệu. Điều này làm cho hệ thống của bạn dự đoán được hơn đáng kể.

Hãy bắt đầu từ những việc nhỏ. Bạn không cần phải tái cấu trúc (refactor) toàn bộ dự án ngay hôm nay. Hãy thử sử dụng Try vào lần tới khi bạn dùng một thư viện ném ra checked exception. Hoặc sử dụng Option trong các DTO để báo hiệu rằng một trường có thể bị trống. Bạn trong tương lai sẽ cảm ơn chính mình khi những vụ sập hệ thống lúc 3 giờ sáng biến mất.

Share: